Compare commits
202 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 296ec5b63e | |||
| 94fa120c73 | |||
| 23618f994f | |||
| ba644415c7 | |||
| 10da5cfdef | |||
| 75b6925deb | |||
| 03a59ff38e | |||
| abf506c1fe | |||
| dfb685f258 | |||
| f3798d0e11 | |||
| 86afd1a46a | |||
| 7171df84fa | |||
| d882a486a9 | |||
| adcbd17ebe | |||
| 3d8de22b96 | |||
| ba4d1e7b21 | |||
| f5a14b8434 | |||
| b74eb7239a | |||
| a8226b35d2 | |||
| 73c708d902 | |||
| 3668f3af9f | |||
| 54697a3240 | |||
| 3428bfbc9e | |||
| cfd1b16801 | |||
| ca70fe9ddc | |||
| 55c587b443 | |||
| b6831c1aca | |||
| 2ac17da7cb | |||
| 274c4cd092 | |||
| 95e1409bfb | |||
| 2c2c53b1f8 | |||
| 9cbfec99df | |||
| 1bd6dcb7e7 | |||
| c1fe8b0162 | |||
| 64c2fb337d | |||
| 147ac02f0d | |||
| 03ef79c0fd | |||
| a261d84fc6 | |||
| abaf1f1454 | |||
| 1e9e21bacf | |||
| ac9f7b9f5e | |||
| a115dd3bd9 | |||
| df9934a4b8 | |||
| 40f18de4d2 | |||
| 13b720442d | |||
| c1f509d65b | |||
| 87f0526f09 | |||
| 060c027325 | |||
| 23bf3cfccb | |||
| 7f5ad3e503 | |||
| 820fa55380 | |||
| eb528c766b | |||
| 53235eb38f | |||
| b9ff5c2e94 | |||
| b7f69d20b6 | |||
| 6c4f4109eb | |||
| 7b5b564a6e | |||
| 695f868b1f | |||
| e724c043d9 | |||
| 491301f58b | |||
| c4f79beb8d | |||
| a613fec2ff | |||
| e54a5d9a13 | |||
| 6d57c8b6f9 | |||
| b67acaccff | |||
| d8568b0e31 | |||
| 373bb2ae99 | |||
| 631286e2d1 | |||
| 74cd7041dc | |||
| 21d920c8b0 | |||
| 44c4df1cd5 | |||
| a4fc0f64b8 | |||
| 9269c7c1c1 | |||
| 403ee63615 | |||
| b622fde291 | |||
| 386fe4eb12 | |||
| 49b7d083f1 | |||
| db4e2915f3 | |||
| 20bdf46792 | |||
| 4ded3f6bfe | |||
| b51113f680 | |||
| be96dea04a | |||
| a786f5df60 | |||
| 9a483a46fa | |||
| 5d2a337332 | |||
| facf776602 | |||
| d3400e3a80 | |||
| 92517bd21e | |||
| 761f635e16 | |||
| 44704ba892 | |||
| 9231c03513 | |||
| 27b32e60b2 | |||
| 9d5b376dcf | |||
| 8fd4883d7e | |||
| d414cec9aa | |||
| 54e3333b72 | |||
| 9f8d0e24df | |||
| 3de7c99301 | |||
| d4fd17cd8f | |||
| d875266db5 | |||
| d696e31b6e | |||
| 90acf2b220 | |||
| 27ef0c6dba | |||
| 8f67435d4a | |||
| b4ebe2440f | |||
| d440a91b0e | |||
| 9e909a3294 | |||
| 75bcf97ab2 | |||
| 0edde8b46f | |||
| bcc34e0bb6 | |||
| 90b41aed89 | |||
| 7930b94981 | |||
| 54a89f6a0e | |||
| 8976a53b05 | |||
| 9849dedf1d | |||
| 547077a808 | |||
| 0b0898dc3c | |||
| 9f9d1ffed8 | |||
| 5a044e4129 | |||
| 35f0ada8a9 | |||
| 39e7ee07ac | |||
| 5a1c9598f1 | |||
| 101a45b6f1 | |||
| fd527f73e6 | |||
| de6aa3eb58 | |||
| c9eafd82ac | |||
| ec57408570 | |||
| 9c38a1b897 | |||
| b34625f511 | |||
| 2e356d3d8f | |||
| 697d449dc8 | |||
| 4f9f61f7a7 | |||
| bceb0a827d | |||
| 2329f712cf | |||
| d2c38702c8 | |||
| def921801f | |||
| 41e006a407 | |||
| 384b514290 | |||
| a6858bd126 | |||
| ef7de2500f | |||
| 6099975b71 | |||
| 828756e8ba | |||
| 4deaf905c1 | |||
| 76c8487a56 | |||
| a05f1233f9 | |||
| 95b833c754 | |||
| 8c057bf928 | |||
| ddf8ade9c6 | |||
| 4cb21b5eb0 | |||
| a03df7d8cc | |||
| 57f66f16f8 | |||
| 737f847c0d | |||
| 671a594945 | |||
| 3646430528 | |||
| e9bd6e576f | |||
| 35300d1c5f | |||
| 112545248e | |||
| 78e6ecc4bc | |||
| 174a6e8e32 | |||
| 20bcabbca4 | |||
| 0a3554cedd | |||
| 2acc7db63d | |||
| 31af7049fc | |||
| 52062a45c1 | |||
| 30406dec6d | |||
| edde27a0a0 | |||
| cfff596c30 | |||
| ba1c968cdf | |||
| c48406ac38 | |||
| 14437477e6 | |||
| 68d928192b | |||
| 028e76eb3f | |||
| cb0c965294 | |||
| d7ce621cb2 | |||
| a1ca4f6eb5 | |||
| 437f0dc46d | |||
| 547eabb4ae | |||
| bb16d3ebc8 | |||
| 1b6170cbc9 | |||
| 4c37a4b7a7 | |||
| 226166bdaf | |||
| 80fa72cabc | |||
| d976d159d0 | |||
| 84ca7fcf40 | |||
| f120a6aab0 | |||
| 0d8108d8da | |||
| e072ebee58 | |||
| c6767d9007 | |||
| bb5760cca4 | |||
| 26b7971ba6 | |||
| b286ede3c6 | |||
| 4ef7afe3ca | |||
| 377f2f0496 | |||
| 7ed5893fc6 | |||
| e70c397e54 | |||
| 6a6be9edde | |||
| 37f8500fe6 | |||
| 207f82dd9d | |||
| de8fedf87a | |||
| 98d306da5b | |||
| c7b7b1247b | |||
| a66f13eb01 |
@@ -31,6 +31,10 @@ xcuserdata
|
|||||||
*.hmap
|
*.hmap
|
||||||
*.ipa
|
*.ipa
|
||||||
|
|
||||||
|
# Swift Package Manager
|
||||||
|
.swiftpm
|
||||||
|
.build/
|
||||||
|
|
||||||
# Fastlane
|
# Fastlane
|
||||||
*.app.dSYM.zip
|
*.app.dSYM.zip
|
||||||
*.mobileprovision
|
*.mobileprovision
|
||||||
@@ -41,7 +45,7 @@ Preview.html
|
|||||||
output
|
output
|
||||||
|
|
||||||
# Wireguard specific
|
# Wireguard specific
|
||||||
WireGuard/WireGuard/Config/Developer.xcconfig
|
Sources/WireGuardApp/Config/Developer.xcconfig
|
||||||
|
|
||||||
# Vim
|
# Vim
|
||||||
.*.sw*
|
.*.sw*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ disabled_rules:
|
|||||||
- type_body_length
|
- type_body_length
|
||||||
- function_body_length
|
- function_body_length
|
||||||
- nesting
|
- nesting
|
||||||
|
- inclusive_language
|
||||||
opt_in_rules:
|
opt_in_rules:
|
||||||
- empty_count
|
- empty_count
|
||||||
- empty_string
|
- empty_string
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -136,5 +136,5 @@ Here's an example WireGuard configuration payload dictionary:
|
|||||||
|
|
||||||
Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once.
|
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-quick(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
|
||||||
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8
|
[wg(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// swift-tools-version:5.3
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "WireGuardKit",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v10_14),
|
||||||
|
.iOS(.v12)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(name: "WireGuardKit", targets: ["WireGuardKit"])
|
||||||
|
],
|
||||||
|
dependencies: [],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "WireGuardKit",
|
||||||
|
dependencies: ["WireGuardKitGo", "WireGuardKitC"]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "WireGuardKitC",
|
||||||
|
dependencies: [],
|
||||||
|
publicHeadersPath: "."
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "WireGuardKitGo",
|
||||||
|
dependencies: [],
|
||||||
|
exclude: [
|
||||||
|
"goruntime-boottime-over-monotonic.diff",
|
||||||
|
"go.mod",
|
||||||
|
"go.sum",
|
||||||
|
"api-apple.go",
|
||||||
|
"Makefile"
|
||||||
|
],
|
||||||
|
publicHeadersPath: ".",
|
||||||
|
linkerSettings: [.linkedLibrary("wg-go")]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
|
||||||
|
|
||||||
|
This project contains an application for iOS and for macOS, as well as many components shared between the two of them. You may toggle between the two platforms by selecting the target from within Xcode.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
- Clone this repo recursively:
|
- Clone this repo:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone --recursive https://git.zx2c4.com/wireguard-ios
|
$ git clone https://git.zx2c4.com/wireguard-apple
|
||||||
$ cd wireguard-ios
|
$ cd wireguard-apple
|
||||||
```
|
```
|
||||||
|
|
||||||
- Rename and populate developer team ID file:
|
- Rename and populate developer team ID file:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/Config/Developer.xcconfig
|
$ cp Sources/WireGuardApp/Config/Developer.xcconfig.template Sources/WireGuardApp/Config/Developer.xcconfig
|
||||||
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
|
$ vim Sources/WireGuardApp/Config/Developer.xcconfig
|
||||||
```
|
```
|
||||||
|
|
||||||
- Install swiftlint and go:
|
- Install swiftlint and go 1.15:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ brew install swiftlint go
|
$ brew install swiftlint go
|
||||||
@@ -25,11 +27,57 @@ $ brew install swiftlint go
|
|||||||
- Open project in Xcode:
|
- Open project in Xcode:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ open ./WireGuard/WireGuard.xcodeproj
|
$ open 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.
|
||||||
|
|
||||||
|
## WireGuardKit integration
|
||||||
|
|
||||||
|
1. Open your Xcode project and add the Swift package with the following URL:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.zx2c4.com/wireguard-apple
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `WireGuardKit` links against `wireguard-go-bridge` library, but it cannot build it automatically
|
||||||
|
due to Swift package manager limitations. So it needs a little help from a developer.
|
||||||
|
Please follow the instructions below to create a build target(s) for `wireguard-go-bridge`.
|
||||||
|
|
||||||
|
- In Xcode, click File -> New -> Target. Switch to "Other" tab and choose "External Build
|
||||||
|
System".
|
||||||
|
- Type in `WireGuardGoBridge<PLATFORM>` under the "Product name", replacing the `<PLATFORM>`
|
||||||
|
placeholder with the name of the platform. For example, when targeting macOS use `macOS`, or
|
||||||
|
when targeting iOS use `iOS`.
|
||||||
|
Make sure the build tool is set to: `/usr/bin/make` (default).
|
||||||
|
- In the appeared "Info" tab of a newly created target, type in the "Directory" path under
|
||||||
|
the "External Build Tool Configuration":
|
||||||
|
|
||||||
|
```
|
||||||
|
${BUILD_DIR%Build/*}SourcePackages/checkouts/wireguard-apple/Sources/WireGuardKitGo
|
||||||
|
```
|
||||||
|
|
||||||
|
- Switch to "Build Settings" and find `SDKROOT`.
|
||||||
|
Type in `macosx` if you target macOS, or type in `iphoneos` if you target iOS.
|
||||||
|
|
||||||
|
3. Go to Xcode project settings and locate your network extension target and switch to
|
||||||
|
"Build Phases" tab.
|
||||||
|
|
||||||
|
- Locate "Dependencies" section and hit "+" to add `WireGuardGoBridge<PLATFORM>` replacing
|
||||||
|
the `<PLATFORM>` placeholder with the name of platform matching the network extension
|
||||||
|
deployment target (i.e macOS or iOS).
|
||||||
|
|
||||||
|
- Locate the "Link with binary libraries" section and hit "+" to add `WireGuardKit`.
|
||||||
|
|
||||||
|
4. In Xcode project settings, locate your main bundle app and switch to "Build Phases" tab.
|
||||||
|
Locate the "Link with binary libraries" section and hit "+" to add `WireGuardKit`.
|
||||||
|
|
||||||
|
5. iOS only: Locate Bitcode settings under your application target, Build settings -> Enable Bitcode,
|
||||||
|
change the corresponding value to "No".
|
||||||
|
|
||||||
|
Note that if you ship your app for both iOS and macOS, make sure to repeat the steps 2-4 twice,
|
||||||
|
once per platform.
|
||||||
|
|
||||||
## MIT License
|
## MIT License
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import os.log
|
import os.log
|
||||||
@@ -35,6 +35,10 @@ extension FileManager {
|
|||||||
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var loginHelperTimestampURL: URL? {
|
||||||
|
return sharedFolderURL?.appendingPathComponent("login-helper-timestamp.bin")
|
||||||
|
}
|
||||||
|
|
||||||
static func deleteFile(at url: URL) -> Bool {
|
static func deleteFile(at url: URL) -> Bool {
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(at: url)
|
try FileManager.default.removeItem(at: url)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Security
|
import Security
|
||||||
@@ -7,9 +7,8 @@ import Security
|
|||||||
class Keychain {
|
class Keychain {
|
||||||
static func openReference(called ref: Data) -> String? {
|
static func openReference(called ref: Data) -> String? {
|
||||||
var result: CFTypeRef?
|
var result: CFTypeRef?
|
||||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
let ret = SecItemCopyMatching([kSecValuePersistentRef: ref,
|
||||||
kSecValuePersistentRef as String: ref,
|
kSecReturnData: true] as CFDictionary,
|
||||||
kSecReturnData as String: true] as CFDictionary,
|
|
||||||
&result)
|
&result)
|
||||||
if ret != errSecSuccess || result == nil {
|
if ret != errSecSuccess || result == nil {
|
||||||
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
|
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
|
||||||
@@ -21,29 +20,30 @@ class Keychain {
|
|||||||
|
|
||||||
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
|
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
|
||||||
var ret: OSStatus
|
var ret: OSStatus
|
||||||
guard var id = Bundle.main.bundleIdentifier else {
|
guard var bundleIdentifier = Bundle.main.bundleIdentifier else {
|
||||||
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
|
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if id.hasSuffix(".network-extension") {
|
if bundleIdentifier.hasSuffix(".network-extension") {
|
||||||
id.removeLast(".network-extension".count)
|
bundleIdentifier.removeLast(".network-extension".count)
|
||||||
}
|
}
|
||||||
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
let itemLabel = "WireGuard Tunnel: \(name)"
|
||||||
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
|
var items: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
||||||
kSecAttrAccount as String: name + ": " + UUID().uuidString,
|
kSecAttrLabel: itemLabel,
|
||||||
kSecAttrDescription as String: "wg-quick(8) config",
|
kSecAttrAccount: name + ": " + UUID().uuidString,
|
||||||
kSecAttrService as String: id,
|
kSecAttrDescription: "wg-quick(8) config",
|
||||||
kSecValueData as String: value.data(using: .utf8) as Any,
|
kSecAttrService: bundleIdentifier,
|
||||||
kSecReturnPersistentRef as String: true]
|
kSecValueData: value.data(using: .utf8) as Any,
|
||||||
|
kSecReturnPersistentRef: true]
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
|
items[kSecAttrAccessGroup] = FileManager.appGroupId
|
||||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
items[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
items[kSecAttrSynchronizable as String] = false
|
items[kSecAttrSynchronizable] = false
|
||||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
items[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||||
|
|
||||||
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else {
|
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex", isDirectory: true).path else {
|
||||||
wg_log(.error, staticMessage: "Unable to determine app extension path")
|
wg_log(.error, staticMessage: "Unable to determine app extension path")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -60,14 +60,12 @@ class Keychain {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var access: SecAccess?
|
var access: SecAccess?
|
||||||
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
|
ret = SecAccessCreate(itemLabel as CFString, [extensionApp!, mainApp!] as CFArray, &access)
|
||||||
[extensionApp!, mainApp!] as CFArray,
|
|
||||||
&access)
|
|
||||||
if ret != errSecSuccess || access == nil {
|
if ret != errSecSuccess || access == nil {
|
||||||
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
items[kSecAttrAccess as String] = access!
|
items[kSecAttrAccess] = access!
|
||||||
#else
|
#else
|
||||||
#error("Unimplemented")
|
#error("Unimplemented")
|
||||||
#endif
|
#endif
|
||||||
@@ -85,7 +83,7 @@ class Keychain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func deleteReference(called ref: Data) {
|
static func deleteReference(called ref: Data) {
|
||||||
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary)
|
let ret = SecItemDelete([kSecValuePersistentRef: ref] as CFDictionary)
|
||||||
if ret != errSecSuccess {
|
if ret != errSecSuccess {
|
||||||
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
||||||
}
|
}
|
||||||
@@ -93,10 +91,10 @@ class Keychain {
|
|||||||
|
|
||||||
static func deleteReferences(except whitelist: Set<Data>) {
|
static func deleteReferences(except whitelist: Set<Data>) {
|
||||||
var result: CFTypeRef?
|
var result: CFTypeRef?
|
||||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
let ret = SecItemCopyMatching([kSecClass: kSecClassGenericPassword,
|
||||||
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
|
kSecAttrService: Bundle.main.bundleIdentifier as Any,
|
||||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
kSecMatchLimit: kSecMatchLimitAll,
|
||||||
kSecReturnPersistentRef as String: true] as CFDictionary,
|
kSecReturnPersistentRef: true] as CFDictionary,
|
||||||
&result)
|
&result)
|
||||||
if ret != errSecSuccess || result == nil {
|
if ret != errSecSuccess || result == nil {
|
||||||
return
|
return
|
||||||
@@ -110,8 +108,7 @@ class Keychain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func verifyReference(called ref: Data) -> Bool {
|
static func verifyReference(called ref: Data) -> Bool {
|
||||||
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
return SecItemCopyMatching([kSecValuePersistentRef: ref] as CFDictionary,
|
||||||
kSecValuePersistentRef as String: ref] as CFDictionary,
|
nil) != errSecItemNotFound
|
||||||
nil) == errSecSuccess
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import os.log
|
import os.log
|
||||||
@@ -49,8 +49,8 @@ public class Logger {
|
|||||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||||
appVersion += " (\(appBuild))"
|
appVersion += " (\(appBuild))"
|
||||||
}
|
}
|
||||||
let goBackendVersion = WIREGUARD_GO_VERSION
|
|
||||||
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
|
Logger.global?.log(message: "App version: \(appVersion)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* SPDX-License-Identifier: MIT
|
/* SPDX-License-Identifier: MIT
|
||||||
*
|
*
|
||||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* SPDX-License-Identifier: MIT
|
/* SPDX-License-Identifier: MIT
|
||||||
*
|
*
|
||||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef RINGLOGGER_H
|
#ifndef RINGLOGGER_H
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
@@ -22,6 +22,9 @@ extension NETunnelProviderProtocol {
|
|||||||
if passwordReference == nil {
|
if passwordReference == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
providerConfiguration = ["UID": getuid()]
|
||||||
|
#endif
|
||||||
|
|
||||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||||
if endpoints.count == 1 {
|
if endpoints.count == 1 {
|
||||||
@@ -60,11 +63,46 @@ extension NETunnelProviderProtocol {
|
|||||||
* in the keychain. But it's still useful to keep the migration
|
* in the keychain. But it's still useful to keep the migration
|
||||||
* around so that .mobileconfig files are easier.
|
* around so that .mobileconfig files are easier.
|
||||||
*/
|
*/
|
||||||
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
|
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
|
||||||
providerConfiguration = nil
|
#if os(macOS)
|
||||||
guard passwordReference == nil else { return true }
|
providerConfiguration = ["UID": getuid()]
|
||||||
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
|
#elseif os(iOS)
|
||||||
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
providerConfiguration = nil
|
||||||
return true
|
#else
|
||||||
|
#error("Unimplemented")
|
||||||
|
#endif
|
||||||
|
guard passwordReference == nil else { return true }
|
||||||
|
wg_log(.info, message: "Migrating tunnel configuration '\(name)'")
|
||||||
|
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
if passwordReference != nil && providerConfiguration?["UID"] == nil && verifyConfigurationReference() {
|
||||||
|
providerConfiguration = ["UID": getuid()]
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
if #available(iOS 15, *) {
|
||||||
|
/* Update the stored reference from the old iOS 14 one to the canonical iOS 15 one.
|
||||||
|
* The iOS 14 ones are 96 bits, while the iOS 15 ones are 160 bits. We do this so
|
||||||
|
* that we can have fast set exclusion in deleteReferences safely. */
|
||||||
|
if passwordReference != nil && passwordReference!.count == 12 {
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let ret = SecItemCopyMatching([kSecValuePersistentRef: passwordReference!,
|
||||||
|
kSecReturnPersistentRef: true] as CFDictionary,
|
||||||
|
&result)
|
||||||
|
if ret != errSecSuccess || result == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard let newReference = result as? Data else { return false }
|
||||||
|
if !newReference.elementsEqual(passwordReference!) {
|
||||||
|
wg_log(.info, message: "Migrating iOS 14-style keychain reference to iOS 15-style keychain reference for '\(name)'")
|
||||||
|
passwordReference = newReference
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ extension TunnelConfiguration {
|
|||||||
var interfaceConfiguration: InterfaceConfiguration?
|
var interfaceConfiguration: InterfaceConfiguration?
|
||||||
var peerConfigurations = [PeerConfiguration]()
|
var peerConfigurations = [PeerConfiguration]()
|
||||||
|
|
||||||
let lines = wgQuickConfig.split(separator: "\n")
|
let lines = wgQuickConfig.split { $0.isNewline }
|
||||||
|
|
||||||
var parserState = ParserState.notInASection
|
var parserState = ParserState.notInASection
|
||||||
var attributes = [String: String]()
|
var attributes = [String: String]()
|
||||||
@@ -111,7 +111,7 @@ extension TunnelConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||||
throw ParseError.multiplePeersWithSamePublicKey
|
throw ParseError.multiplePeersWithSamePublicKey
|
||||||
}
|
}
|
||||||
@@ -125,9 +125,7 @@ extension TunnelConfiguration {
|
|||||||
|
|
||||||
func asWgQuickConfig() -> String {
|
func asWgQuickConfig() -> String {
|
||||||
var output = "[Interface]\n"
|
var output = "[Interface]\n"
|
||||||
if let privateKey = interface.privateKey.base64Key() {
|
output.append("PrivateKey = \(interface.privateKey.base64Key)\n")
|
||||||
output.append("PrivateKey = \(privateKey)\n")
|
|
||||||
}
|
|
||||||
if let listenPort = interface.listenPort {
|
if let listenPort = interface.listenPort {
|
||||||
output.append("ListenPort = \(listenPort)\n")
|
output.append("ListenPort = \(listenPort)\n")
|
||||||
}
|
}
|
||||||
@@ -135,8 +133,10 @@ extension TunnelConfiguration {
|
|||||||
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||||
output.append("Address = \(addressString)\n")
|
output.append("Address = \(addressString)\n")
|
||||||
}
|
}
|
||||||
if !interface.dns.isEmpty {
|
if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
|
||||||
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
var dnsLine = interface.dns.map { $0.stringRepresentation }
|
||||||
|
dnsLine.append(contentsOf: interface.dnsSearch)
|
||||||
|
let dnsString = dnsLine.joined(separator: ", ")
|
||||||
output.append("DNS = \(dnsString)\n")
|
output.append("DNS = \(dnsString)\n")
|
||||||
}
|
}
|
||||||
if let mtu = interface.mtu {
|
if let mtu = interface.mtu {
|
||||||
@@ -145,10 +145,8 @@ extension TunnelConfiguration {
|
|||||||
|
|
||||||
for peer in peers {
|
for peer in peers {
|
||||||
output.append("\n[Peer]\n")
|
output.append("\n[Peer]\n")
|
||||||
if let publicKey = peer.publicKey.base64Key() {
|
output.append("PublicKey = \(peer.publicKey.base64Key)\n")
|
||||||
output.append("PublicKey = \(publicKey)\n")
|
if let preSharedKey = peer.preSharedKey?.base64Key {
|
||||||
}
|
|
||||||
if let preSharedKey = peer.preSharedKey?.base64Key() {
|
|
||||||
output.append("PresharedKey = \(preSharedKey)\n")
|
output.append("PresharedKey = \(preSharedKey)\n")
|
||||||
}
|
}
|
||||||
if !peer.allowedIPs.isEmpty {
|
if !peer.allowedIPs.isEmpty {
|
||||||
@@ -170,7 +168,7 @@ extension TunnelConfiguration {
|
|||||||
guard let privateKeyString = attributes["privatekey"] else {
|
guard let privateKeyString = attributes["privatekey"] else {
|
||||||
throw ParseError.interfaceHasNoPrivateKey
|
throw ParseError.interfaceHasNoPrivateKey
|
||||||
}
|
}
|
||||||
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
|
||||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||||
}
|
}
|
||||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||||
@@ -192,13 +190,16 @@ extension TunnelConfiguration {
|
|||||||
}
|
}
|
||||||
if let dnsString = attributes["dns"] {
|
if let dnsString = attributes["dns"] {
|
||||||
var dnsServers = [DNSServer]()
|
var dnsServers = [DNSServer]()
|
||||||
|
var dnsSearch = [String]()
|
||||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||||
guard let dnsServer = DNSServer(from: dnsServerString) else {
|
if let dnsServer = DNSServer(from: dnsServerString) {
|
||||||
throw ParseError.interfaceHasInvalidDNS(dnsServerString)
|
dnsServers.append(dnsServer)
|
||||||
|
} else {
|
||||||
|
dnsSearch.append(dnsServerString)
|
||||||
}
|
}
|
||||||
dnsServers.append(dnsServer)
|
|
||||||
}
|
}
|
||||||
interface.dns = dnsServers
|
interface.dns = dnsServers
|
||||||
|
interface.dnsSearch = dnsSearch
|
||||||
}
|
}
|
||||||
if let mtuString = attributes["mtu"] {
|
if let mtuString = attributes["mtu"] {
|
||||||
guard let mtu = UInt16(mtuString) else {
|
guard let mtu = UInt16(mtuString) else {
|
||||||
@@ -213,12 +214,12 @@ extension TunnelConfiguration {
|
|||||||
guard let publicKeyString = attributes["publickey"] else {
|
guard let publicKeyString = attributes["publickey"] else {
|
||||||
throw ParseError.peerHasNoPublicKey
|
throw ParseError.peerHasNoPublicKey
|
||||||
}
|
}
|
||||||
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
guard let publicKey = PublicKey(base64Key: publicKeyString) else {
|
||||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||||
}
|
}
|
||||||
var peer = PeerConfiguration(publicKey: publicKey)
|
var peer = PeerConfiguration(publicKey: publicKey)
|
||||||
if let preSharedKeyString = attributes["presharedkey"] {
|
if let preSharedKeyString = attributes["presharedkey"] {
|
||||||
guard let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
guard let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) else {
|
||||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||||
}
|
}
|
||||||
peer.preSharedKey = preSharedKey
|
peer.preSharedKey = preSharedKey
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// This source file contains bits of code from:
|
||||||
|
/// https://oleb.net/blog/2018/01/notificationcenter-removeobserver/
|
||||||
|
|
||||||
|
/// Wraps the observer token received from
|
||||||
|
/// `NotificationCenter.addObserver(forName:object:queue:using:)`
|
||||||
|
/// and unregisters it in deinit.
|
||||||
|
final class NotificationToken {
|
||||||
|
let notificationCenter: NotificationCenter
|
||||||
|
let token: Any
|
||||||
|
|
||||||
|
init(notificationCenter: NotificationCenter = .default, token: Any) {
|
||||||
|
self.notificationCenter = notificationCenter
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
notificationCenter.removeObserver(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NotificationCenter {
|
||||||
|
/// Convenience wrapper for addObserver(forName:object:queue:using:)
|
||||||
|
/// that returns our custom `NotificationToken`.
|
||||||
|
func observe(name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NotificationToken {
|
||||||
|
let token = addObserver(forName: name, object: obj, queue: queue, using: block)
|
||||||
|
return NotificationToken(notificationCenter: self, token: token)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
// iOS permission prompts
|
// iOS permission prompts
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
// Generic alert action names
|
// Generic alert action names
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"tunnelsListSelectAllButtonTitle" = "Select All";
|
"tunnelsListSelectAllButtonTitle" = "Select All";
|
||||||
"tunnelsListDeleteButtonTitle" = "Delete";
|
"tunnelsListDeleteButtonTitle" = "Delete";
|
||||||
"tunnelsListSelectedTitle (%d)" = "%d selected";
|
"tunnelsListSelectedTitle (%d)" = "%d selected";
|
||||||
|
"tunnelListCaptionOnDemand" = "On-Demand";
|
||||||
|
|
||||||
// Tunnels list menu
|
// Tunnels list menu
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@
|
|||||||
"tunnelStatusRestarting" = "Restarting";
|
"tunnelStatusRestarting" = "Restarting";
|
||||||
"tunnelStatusWaiting" = "Waiting";
|
"tunnelStatusWaiting" = "Waiting";
|
||||||
|
|
||||||
|
"tunnelStatusAddendumOnDemand" = " (On-Demand)";
|
||||||
|
"tunnelStatusOnDemandDisabled" = "On-Demand Disabled";
|
||||||
|
"tunnelStatusAddendumOnDemandEnabled" = ", On-Demand Enabled";
|
||||||
|
"tunnelStatusAddendumOnDemandDisabled" = ", On-Demand Disabled";
|
||||||
|
|
||||||
"macToggleStatusButtonActivate" = "Activate";
|
"macToggleStatusButtonActivate" = "Activate";
|
||||||
"macToggleStatusButtonActivating" = "Activating…";
|
"macToggleStatusButtonActivating" = "Activating…";
|
||||||
"macToggleStatusButtonDeactivate" = "Deactivate";
|
"macToggleStatusButtonDeactivate" = "Deactivate";
|
||||||
@@ -62,6 +68,9 @@
|
|||||||
"macToggleStatusButtonReasserting" = "Reactivating…";
|
"macToggleStatusButtonReasserting" = "Reactivating…";
|
||||||
"macToggleStatusButtonRestarting" = "Restarting…";
|
"macToggleStatusButtonRestarting" = "Restarting…";
|
||||||
"macToggleStatusButtonWaiting" = "Waiting…";
|
"macToggleStatusButtonWaiting" = "Waiting…";
|
||||||
|
"macToggleStatusButtonEnableOnDemand" = "Enable On-Demand";
|
||||||
|
"macToggleStatusButtonDisableOnDemand" = "Disable On-Demand";
|
||||||
|
"macToggleStatusButtonDisableOnDemandDeactivate" = "Disable On-Demand and Deactivate";
|
||||||
|
|
||||||
"tunnelSectionTitleInterface" = "Interface";
|
"tunnelSectionTitleInterface" = "Interface";
|
||||||
|
|
||||||
@@ -109,8 +118,9 @@
|
|||||||
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
|
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
|
||||||
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
|
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
|
||||||
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
|
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
|
||||||
|
"tunnelOnDemandSSIDTextFieldPlaceholder" = "SSID";
|
||||||
|
|
||||||
"tunnelOnDemandKey" = "On demand";
|
"tunnelOnDemandKey" = "On-demand";
|
||||||
"tunnelOnDemandOptionOff" = "Off";
|
"tunnelOnDemandOptionOff" = "Off";
|
||||||
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
||||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||||
@@ -255,8 +265,6 @@
|
|||||||
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
|
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
|
||||||
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
|
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
|
||||||
|
|
||||||
"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
|
|
||||||
|
|
||||||
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
|
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
|
||||||
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
|
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
|
||||||
|
|
||||||
@@ -297,6 +305,7 @@
|
|||||||
"macMenuNetworksNone" = "Networks: None";
|
"macMenuNetworksNone" = "Networks: None";
|
||||||
|
|
||||||
"macMenuTitle" = "WireGuard";
|
"macMenuTitle" = "WireGuard";
|
||||||
|
"macTunnelsMenuTitle" = "Tunnels";
|
||||||
"macMenuManageTunnels" = "Manage Tunnels";
|
"macMenuManageTunnels" = "Manage Tunnels";
|
||||||
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
|
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
|
||||||
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
|
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
|
||||||
@@ -349,6 +358,8 @@
|
|||||||
|
|
||||||
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
|
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
|
||||||
|
|
||||||
|
"macButtonEdit" = "Edit";
|
||||||
|
|
||||||
// Mac detail/edit view fields
|
// Mac detail/edit view fields
|
||||||
|
|
||||||
"macFieldKey (%@)" = "%@:";
|
"macFieldKey (%@)" = "%@:";
|
||||||
@@ -413,8 +424,6 @@
|
|||||||
|
|
||||||
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
||||||
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
||||||
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
|
|
||||||
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
|
|
||||||
|
|
||||||
// Mac tooltip
|
// Mac tooltip
|
||||||
|
|
||||||
@@ -439,3 +448,7 @@
|
|||||||
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
|
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
|
||||||
"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel ‘%@’, deactivate it, and then continue updating in App Store.";
|
"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel ‘%@’, deactivate it, and then continue updating in App Store.";
|
||||||
"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel ‘%@’ and then continue updating in App Store.";
|
"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel ‘%@’ and then continue updating in App Store.";
|
||||||
|
|
||||||
|
// Donation
|
||||||
|
|
||||||
|
"donateLink" = "♥ Donate to the WireGuard Project";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VERSION_NAME = 1.0.15
|
||||||
|
VERSION_ID = 26
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 953 B |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
@@ -42,50 +42,63 @@ extension ActivateOnDemandOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tunnelProviderManager.onDemandRules = rules
|
tunnelProviderManager.onDemandRules = rules
|
||||||
tunnelProviderManager.isOnDemandEnabled = self != .off
|
tunnelProviderManager.isOnDemandEnabled = (rules != nil) && tunnelProviderManager.isOnDemandEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
if let onDemandRules = tunnelProviderManager.onDemandRules {
|
||||||
let activateOnDemandOption: ActivateOnDemandOption
|
self = ActivateOnDemandOption.create(from: onDemandRules)
|
||||||
|
} else {
|
||||||
|
self = .off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
|
||||||
switch rules.count {
|
switch rules.count {
|
||||||
case 0:
|
case 0:
|
||||||
activateOnDemandOption = .off
|
return .off
|
||||||
case 1:
|
case 1:
|
||||||
let rule = rules[0]
|
let rule = rules[0]
|
||||||
precondition(rule.action == .connect)
|
guard rule.action == .connect else { return .off }
|
||||||
activateOnDemandOption = .anyInterface(.anySSID)
|
return .anyInterface(.anySSID)
|
||||||
case 2:
|
case 2:
|
||||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
guard let connectRule = rules.first(where: { $0.action == .connect }) else {
|
||||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
|
||||||
|
return .off
|
||||||
|
}
|
||||||
|
guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
|
||||||
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
|
||||||
|
return .off
|
||||||
|
}
|
||||||
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
|
return .wiFiInterfaceOnly(.anySSID)
|
||||||
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||||
activateOnDemandOption = .nonWiFiInterfaceOnly
|
return .nonWiFiInterfaceOnly
|
||||||
} else {
|
} else {
|
||||||
fatalError("Unexpected onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
case 3:
|
case 3:
|
||||||
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
|
guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
|
||||||
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
|
guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
|
||||||
let ssids = ssidRule.ssidMatch!
|
let ssids = ssidRule.ssidMatch!
|
||||||
switch (ssidRule.action, nonWiFiRule.action) {
|
switch (ssidRule.action, nonWiFiRule.action) {
|
||||||
case (.connect, .connect):
|
case (.connect, .connect):
|
||||||
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
|
return .anyInterface(.onlySpecificSSIDs(ssids))
|
||||||
case (.connect, .disconnect):
|
case (.connect, .disconnect):
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||||
case (.disconnect, .connect):
|
case (.disconnect, .connect):
|
||||||
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
|
return .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||||
case (.disconnect, .disconnect):
|
case (.disconnect, .disconnect):
|
||||||
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||||
default:
|
default:
|
||||||
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
|
wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
|
||||||
|
return .off
|
||||||
}
|
}
|
||||||
|
|
||||||
self = activateOnDemandOption
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
@@ -26,11 +26,11 @@ class MockTunnels {
|
|||||||
static func createMockTunnels() -> [NETunnelProviderManager] {
|
static func createMockTunnels() -> [NETunnelProviderManager] {
|
||||||
return tunnelNames.map { tunnelName -> NETunnelProviderManager in
|
return tunnelNames.map { tunnelName -> NETunnelProviderManager in
|
||||||
|
|
||||||
var interface = InterfaceConfiguration(privateKey: Curve25519.generatePrivateKey())
|
var interface = InterfaceConfiguration(privateKey: PrivateKey())
|
||||||
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
|
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
|
||||||
interface.dns = dnsServers.map { DNSServer(from: $0)! }
|
interface.dns = dnsServers.map { DNSServer(from: $0)! }
|
||||||
|
|
||||||
var peer = PeerConfiguration(publicKey: Curve25519.generatePublicKey(fromPrivateKey: Curve25519.generatePrivateKey()))
|
var peer = PeerConfiguration(publicKey: PrivateKey().publicKey)
|
||||||
peer.endpoint = Endpoint(from: endpoint)
|
peer.endpoint = Endpoint(from: endpoint)
|
||||||
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
|
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -67,13 +67,14 @@ extension TunnelConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||||
throw ParseError.multiplePeersWithSamePublicKey
|
throw ParseError.multiplePeersWithSamePublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
|
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
|
||||||
interfaceConfiguration?.dns = base?.interface.dns ?? []
|
interfaceConfiguration?.dns = base?.interface.dns ?? []
|
||||||
|
interfaceConfiguration?.dnsSearch = base?.interface.dnsSearch ?? []
|
||||||
interfaceConfiguration?.mtu = base?.interface.mtu
|
interfaceConfiguration?.mtu = base?.interface.mtu
|
||||||
|
|
||||||
if let interfaceConfiguration = interfaceConfiguration {
|
if let interfaceConfiguration = interfaceConfiguration {
|
||||||
@@ -87,7 +88,7 @@ extension TunnelConfiguration {
|
|||||||
guard let privateKeyString = attributes["private_key"] else {
|
guard let privateKeyString = attributes["private_key"] else {
|
||||||
throw ParseError.interfaceHasNoPrivateKey
|
throw ParseError.interfaceHasNoPrivateKey
|
||||||
}
|
}
|
||||||
guard let privateKey = Data(hexKey: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
guard let privateKey = PrivateKey(hexKey: privateKeyString) else {
|
||||||
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
|
||||||
}
|
}
|
||||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||||
@@ -106,18 +107,18 @@ extension TunnelConfiguration {
|
|||||||
guard let publicKeyString = attributes["public_key"] else {
|
guard let publicKeyString = attributes["public_key"] else {
|
||||||
throw ParseError.peerHasNoPublicKey
|
throw ParseError.peerHasNoPublicKey
|
||||||
}
|
}
|
||||||
guard let publicKey = Data(hexKey: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
guard let publicKey = PublicKey(hexKey: publicKeyString) else {
|
||||||
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
|
||||||
}
|
}
|
||||||
var peer = PeerConfiguration(publicKey: publicKey)
|
var peer = PeerConfiguration(publicKey: publicKey)
|
||||||
if let preSharedKeyString = attributes["preshared_key"] {
|
if let preSharedKeyString = attributes["preshared_key"] {
|
||||||
guard let preSharedKey = Data(hexKey: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
|
guard let preSharedKey = PreSharedKey(hexKey: preSharedKeyString) else {
|
||||||
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
|
||||||
}
|
}
|
||||||
// TODO(zx2c4): does the compiler optimize this away?
|
// TODO(zx2c4): does the compiler optimize this away?
|
||||||
var accumulator: UInt8 = 0
|
var accumulator: UInt8 = 0
|
||||||
for index in 0..<preSharedKey.count {
|
for index in 0..<preSharedKey.rawValue.count {
|
||||||
accumulator |= preSharedKey[index]
|
accumulator |= preSharedKey.rawValue[index]
|
||||||
}
|
}
|
||||||
if accumulator != 0 {
|
if accumulator != 0 {
|
||||||
peer.preSharedKey = preSharedKey
|
peer.preSharedKey = preSharedKey
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ enum TunnelsManagerActivationError: WireGuardAppError {
|
|||||||
|
|
||||||
var alertText: AlertText {
|
var alertText: AlertText {
|
||||||
switch self {
|
switch self {
|
||||||
case .activationFailed(let wasOnDemandEnabled):
|
case .activationFailed:
|
||||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
|
||||||
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
|
case .activationFailedWithExtensionError(let title, let message, _):
|
||||||
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
return (title, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
protocol TunnelsManagerListDelegate: class {
|
protocol TunnelsManagerListDelegate: AnyObject {
|
||||||
func tunnelAdded(at index: Int)
|
func tunnelAdded(at index: Int)
|
||||||
func tunnelModified(at index: Int)
|
func tunnelModified(at index: Int)
|
||||||
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
||||||
func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
|
func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol TunnelsManagerActivationDelegate: class {
|
protocol TunnelsManagerActivationDelegate: AnyObject {
|
||||||
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
|
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
|
||||||
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
|
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
|
||||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
|
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
|
||||||
@@ -23,9 +23,9 @@ class TunnelsManager {
|
|||||||
private var tunnels: [TunnelContainer]
|
private var tunnels: [TunnelContainer]
|
||||||
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
||||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||||
private var statusObservationToken: AnyObject?
|
private var statusObservationToken: NotificationToken?
|
||||||
private var waiteeObservationToken: AnyObject?
|
private var waiteeObservationToken: NSKeyValueObservation?
|
||||||
private var configurationsObservationToken: AnyObject?
|
private var configurationsObservationToken: NotificationToken?
|
||||||
|
|
||||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||||
@@ -58,13 +58,19 @@ class TunnelsManager {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
let passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
|
let passwordRef: Data?
|
||||||
|
if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
|
||||||
|
passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
||||||
|
} else {
|
||||||
|
passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
#error("Unimplemented")
|
#error("Unimplemented")
|
||||||
#endif
|
#endif
|
||||||
if let ref = passwordRef {
|
if let ref = passwordRef {
|
||||||
refs.insert(ref)
|
refs.insert(ref)
|
||||||
} else {
|
} else {
|
||||||
|
wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
|
||||||
tunnelManager.removeFromPreferences { _ in }
|
tunnelManager.removeFromPreferences { _ in }
|
||||||
tunnelManagers.remove(at: index)
|
tunnelManagers.remove(at: index)
|
||||||
}
|
}
|
||||||
@@ -132,10 +138,10 @@ class TunnelsManager {
|
|||||||
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
|
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
|
||||||
|
|
||||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||||
guard error == nil else {
|
if let error = error {
|
||||||
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
wg_log(.error, message: "Add: Saving configuration failed: \(error)")
|
||||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
|
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +169,20 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||||
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
|
// Temporarily pause observation of changes to VPN configurations to prevent the feedback
|
||||||
|
// loop that causes `reload()` to be called on each newly added tunnel, which significantly
|
||||||
|
// impacts performance.
|
||||||
|
configurationsObservationToken = nil
|
||||||
|
|
||||||
|
self.addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil) { [weak self] numSucceeded, error in
|
||||||
|
completionHandler(numSucceeded, error)
|
||||||
|
|
||||||
|
// Restart observation of changes to VPN configrations.
|
||||||
|
self?.startObservingTunnelConfigurations()
|
||||||
|
|
||||||
|
// Force reload all configurations to make sure that all tunnels are up to date.
|
||||||
|
self?.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
|
||||||
@@ -187,7 +206,10 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
|
||||||
|
onDemandOption: ActivateOnDemandOption,
|
||||||
|
shouldEnsureOnDemandEnabled: Bool = false,
|
||||||
|
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
let tunnelName = tunnelConfiguration.name ?? ""
|
let tunnelName = tunnelConfiguration.name ?? ""
|
||||||
if tunnelName.isEmpty {
|
if tunnelName.isEmpty {
|
||||||
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
completionHandler(TunnelsManagerError.tunnelNameEmpty)
|
||||||
@@ -195,6 +217,20 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let tunnelProviderManager = tunnel.tunnelProvider
|
let tunnelProviderManager = tunnel.tunnelProvider
|
||||||
|
|
||||||
|
let isIntroducingOnDemandRules = (tunnelProviderManager.onDemandRules ?? []).isEmpty && onDemandOption != .off
|
||||||
|
if isIntroducingOnDemandRules && tunnel.status != .inactive && tunnel.status != .deactivating {
|
||||||
|
tunnel.onDeactivated = { [weak self] in
|
||||||
|
self?.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration,
|
||||||
|
onDemandOption: onDemandOption, shouldEnsureOnDemandEnabled: true,
|
||||||
|
completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
self.startDeactivation(of: tunnel)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
tunnel.onDeactivated = nil
|
||||||
|
}
|
||||||
|
|
||||||
let oldName = tunnelProviderManager.localizedDescription ?? ""
|
let oldName = tunnelProviderManager.localizedDescription ?? ""
|
||||||
let isNameChanged = tunnelName != oldName
|
let isNameChanged = tunnelName != oldName
|
||||||
if isNameChanged {
|
if isNameChanged {
|
||||||
@@ -212,14 +248,17 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
tunnelProviderManager.isEnabled = true
|
tunnelProviderManager.isEnabled = true
|
||||||
|
|
||||||
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
|
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && shouldEnsureOnDemandEnabled
|
||||||
onDemandOption.apply(on: tunnelProviderManager)
|
onDemandOption.apply(on: tunnelProviderManager)
|
||||||
|
if shouldEnsureOnDemandEnabled {
|
||||||
|
tunnelProviderManager.isOnDemandEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||||
guard error == nil else {
|
if let error = error {
|
||||||
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
|
// 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!)")
|
wg_log(.error, message: "Modify: Saving configuration failed: \(error)")
|
||||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -247,12 +286,12 @@ class TunnelsManager {
|
|||||||
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
|
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
|
||||||
tunnelProviderManager.loadFromPreferences { error in
|
tunnelProviderManager.loadFromPreferences { error in
|
||||||
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||||
guard error == nil else {
|
if let error = error {
|
||||||
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
|
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error)")
|
||||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||||
return
|
} else {
|
||||||
|
completionHandler(nil)
|
||||||
}
|
}
|
||||||
completionHandler(nil)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
completionHandler(nil)
|
completionHandler(nil)
|
||||||
@@ -262,14 +301,19 @@ class TunnelsManager {
|
|||||||
|
|
||||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
let tunnelProviderManager = tunnel.tunnelProvider
|
let tunnelProviderManager = tunnel.tunnelProvider
|
||||||
if tunnel.isTunnelConfigurationAvailableInKeychain {
|
#if os(macOS)
|
||||||
|
if tunnel.isTunnelAvailableToUser {
|
||||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||||
}
|
}
|
||||||
|
#elseif os(iOS)
|
||||||
|
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||||
|
#else
|
||||||
|
#error("Unimplemented")
|
||||||
|
#endif
|
||||||
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
||||||
guard error == nil else {
|
if let error = error {
|
||||||
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
wg_log(.error, message: "Remove: Saving configuration failed: \(error)")
|
||||||
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
|
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
|
if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
|
||||||
@@ -285,7 +329,20 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
|
// Temporarily pause observation of changes to VPN configurations to prevent the feedback
|
||||||
|
// loop that causes `reload()` to be called for each removed tunnel, which significantly
|
||||||
|
// impacts performance.
|
||||||
|
configurationsObservationToken = nil
|
||||||
|
|
||||||
|
removeMultiple(tunnels: ArraySlice(tunnels)) { [weak self] error in
|
||||||
|
completionHandler(error)
|
||||||
|
|
||||||
|
// Restart observation of changes to VPN configrations.
|
||||||
|
self?.startObservingTunnelConfigurations()
|
||||||
|
|
||||||
|
// Force reload all configurations to make sure that all tunnels are up to date.
|
||||||
|
self?.reload()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
@@ -305,6 +362,41 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setOnDemandEnabled(_ isOnDemandEnabled: Bool, on tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||||
|
let tunnelProviderManager = tunnel.tunnelProvider
|
||||||
|
let isCurrentlyEnabled = (tunnelProviderManager.isOnDemandEnabled && tunnelProviderManager.isEnabled)
|
||||||
|
guard isCurrentlyEnabled != isOnDemandEnabled else {
|
||||||
|
completionHandler(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && isOnDemandEnabled
|
||||||
|
tunnelProviderManager.isOnDemandEnabled = isOnDemandEnabled
|
||||||
|
tunnelProviderManager.isEnabled = true
|
||||||
|
tunnelProviderManager.saveToPreferences { error in
|
||||||
|
if let error = error {
|
||||||
|
wg_log(.error, message: "Modify On-Demand: Saving configuration failed: \(error)")
|
||||||
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isActivatingOnDemand {
|
||||||
|
// If we're enabling on-demand, we want to make sure the tunnel is enabled.
|
||||||
|
// If not enabled, the OS will not turn the tunnel on/off based on our rules.
|
||||||
|
tunnelProviderManager.loadFromPreferences { error in
|
||||||
|
// isActivateOnDemandEnabled will get changed in reload(), but no harm in setting it here too
|
||||||
|
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||||
|
if let error = error {
|
||||||
|
wg_log(.error, message: "Modify On-Demand: Re-loading after saving configuration failed: \(error)")
|
||||||
|
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func numberOfTunnels() -> Int {
|
func numberOfTunnels() -> Int {
|
||||||
return tunnels.count
|
return tunnels.count
|
||||||
}
|
}
|
||||||
@@ -352,7 +444,17 @@ class TunnelsManager {
|
|||||||
tunnel.status = .waiting
|
tunnel.status = .waiting
|
||||||
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
|
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
|
||||||
if tunnelInOperation.status != .deactivating {
|
if tunnelInOperation.status != .deactivating {
|
||||||
startDeactivation(of: tunnelInOperation)
|
if tunnelInOperation.isActivateOnDemandEnabled {
|
||||||
|
setOnDemandEnabled(false, on: tunnelInOperation) { [weak self] error in
|
||||||
|
guard error == nil else {
|
||||||
|
wg_log(.error, message: "Unable to activate tunnel '\(tunnel.name)' because on-demand could not be disabled on active tunnel '\(tunnel.name)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self?.startDeactivation(of: tunnelInOperation)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startDeactivation(of: tunnelInOperation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -395,7 +497,7 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func startObservingTunnelStatuses() {
|
private func startObservingTunnelStatuses() {
|
||||||
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
|
statusObservationToken = NotificationCenter.default.observe(name: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
||||||
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
||||||
@@ -417,6 +519,11 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if session.status == .disconnected {
|
||||||
|
tunnel.onDeactivated?()
|
||||||
|
tunnel.onDeactivated = nil
|
||||||
|
}
|
||||||
|
|
||||||
if tunnel.status == .restarting && session.status == .disconnected {
|
if tunnel.status == .restarting && session.status == .disconnected {
|
||||||
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
tunnel.startActivation(activationDelegate: self.activationDelegate)
|
||||||
return
|
return
|
||||||
@@ -427,7 +534,7 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startObservingTunnelConfigurations() {
|
func startObservingTunnelConfigurations() {
|
||||||
configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
configurationsObservationToken = NotificationCenter.default.observe(name: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
// We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
|
// 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
|
// (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
|
||||||
@@ -438,8 +545,8 @@ class TunnelsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
|
static func tunnelNameIsLessThan(_ lhs: String, _ rhs: String) -> Bool {
|
||||||
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
return lhs.compare(rhs, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +568,7 @@ class TunnelContainer: NSObject {
|
|||||||
@objc dynamic var status: TunnelStatus
|
@objc dynamic var status: TunnelStatus
|
||||||
|
|
||||||
@objc dynamic var isActivateOnDemandEnabled: Bool
|
@objc dynamic var isActivateOnDemandEnabled: Bool
|
||||||
|
@objc dynamic var hasOnDemandRules: Bool
|
||||||
|
|
||||||
var isAttemptingActivation = false {
|
var isAttemptingActivation = false {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -486,26 +594,35 @@ class TunnelContainer: NSObject {
|
|||||||
var activationAttemptId: String?
|
var activationAttemptId: String?
|
||||||
var activationTimer: Timer?
|
var activationTimer: Timer?
|
||||||
var deactivationTimer: Timer?
|
var deactivationTimer: Timer?
|
||||||
|
var onDeactivated: (() -> Void)?
|
||||||
|
|
||||||
fileprivate var tunnelProvider: NETunnelProviderManager
|
fileprivate var tunnelProvider: NETunnelProviderManager {
|
||||||
|
didSet {
|
||||||
|
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled && tunnelProvider.isEnabled
|
||||||
|
hasOnDemandRules = !(tunnelProvider.onDemandRules ?? []).isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var tunnelConfiguration: TunnelConfiguration? {
|
var tunnelConfiguration: TunnelConfiguration? {
|
||||||
return tunnelProvider.tunnelConfiguration
|
return tunnelProvider.tunnelConfiguration
|
||||||
}
|
}
|
||||||
|
|
||||||
var isTunnelConfigurationAvailableInKeychain: Bool {
|
|
||||||
return tunnelProvider.isTunnelConfigurationAvailableInKeychain
|
|
||||||
}
|
|
||||||
|
|
||||||
var onDemandOption: ActivateOnDemandOption {
|
var onDemandOption: ActivateOnDemandOption {
|
||||||
return ActivateOnDemandOption(from: tunnelProvider)
|
return ActivateOnDemandOption(from: tunnelProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
var isTunnelAvailableToUser: Bool {
|
||||||
|
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
init(tunnel: NETunnelProviderManager) {
|
init(tunnel: NETunnelProviderManager) {
|
||||||
name = tunnel.localizedDescription ?? "Unnamed"
|
name = tunnel.localizedDescription ?? "Unnamed"
|
||||||
let status = TunnelStatus(from: tunnel.connection.status)
|
let status = TunnelStatus(from: tunnel.connection.status)
|
||||||
self.status = status
|
self.status = status
|
||||||
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled && tunnel.isEnabled
|
||||||
|
hasOnDemandRules = !(tunnel.onDemandRules ?? []).isEmpty
|
||||||
tunnelProvider = tunnel
|
tunnelProvider = tunnel
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@@ -528,11 +645,10 @@ class TunnelContainer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func refreshStatus() {
|
func refreshStatus() {
|
||||||
if status == .restarting {
|
if (status == .restarting) || (status == .waiting && tunnelProvider.connection.status == .disconnected) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status = TunnelStatus(from: tunnelProvider.connection.status)
|
status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||||
@@ -609,18 +725,8 @@ class TunnelContainer: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension NETunnelProviderManager {
|
extension NETunnelProviderManager {
|
||||||
private static var cachedIsConfigAvailableInKeychainKey: UInt8 = 0
|
|
||||||
private static var cachedConfigKey: UInt8 = 0
|
private static var cachedConfigKey: UInt8 = 0
|
||||||
|
|
||||||
var isTunnelConfigurationAvailableInKeychain: Bool {
|
|
||||||
if let cachedNumber = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey) as? NSNumber {
|
|
||||||
return cachedNumber.boolValue
|
|
||||||
}
|
|
||||||
let isAvailable = (protocolConfiguration as? NETunnelProviderProtocol)?.verifyConfigurationReference() ?? false
|
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: isAvailable), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
return isAvailable
|
|
||||||
}
|
|
||||||
|
|
||||||
var tunnelConfiguration: TunnelConfiguration? {
|
var tunnelConfiguration: TunnelConfiguration? {
|
||||||
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
||||||
return cached
|
return cached
|
||||||
@@ -636,17 +742,9 @@ extension NETunnelProviderManager {
|
|||||||
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
||||||
localizedDescription = tunnelConfiguration.name
|
localizedDescription = tunnelConfiguration.name
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: true), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
|
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
|
||||||
switch (isTunnelConfigurationAvailableInKeychain, tunnel.isTunnelConfigurationAvailableInKeychain) {
|
return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
|
||||||
case (true, true):
|
|
||||||
return tunnelConfiguration == tunnel.tunnelConfiguration
|
|
||||||
case (false, false):
|
|
||||||
return localizedDescription == tunnel.name
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -51,20 +51,18 @@ class ActivateOnDemandViewModel {
|
|||||||
extension ActivateOnDemandViewModel {
|
extension ActivateOnDemandViewModel {
|
||||||
convenience init(tunnel: TunnelContainer) {
|
convenience init(tunnel: TunnelContainer) {
|
||||||
self.init()
|
self.init()
|
||||||
if tunnel.isActivateOnDemandEnabled {
|
switch tunnel.onDemandOption {
|
||||||
switch tunnel.onDemandOption {
|
case .off:
|
||||||
case .off:
|
break
|
||||||
break
|
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
||||||
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
isWiFiInterfaceEnabled = true
|
||||||
isWiFiInterfaceEnabled = true
|
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
case .nonWiFiInterfaceOnly:
|
||||||
case .nonWiFiInterfaceOnly:
|
isNonWiFiInterfaceEnabled = true
|
||||||
isNonWiFiInterfaceEnabled = true
|
case .anyInterface(let onDemandSSIDOption):
|
||||||
case .anyInterface(let onDemandSSIDOption):
|
isWiFiInterfaceEnabled = true
|
||||||
isWiFiInterfaceEnabled = true
|
isNonWiFiInterfaceEnabled = true
|
||||||
isNonWiFiInterfaceEnabled = true
|
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
protocol ErrorPresenterProtocol {
|
protocol ErrorPresenterProtocol {
|
||||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
|
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -44,10 +44,20 @@ class TunnelImporter {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
var parseError: Error?
|
||||||
|
var tunnelConfiguration: TunnelConfiguration?
|
||||||
|
do {
|
||||||
|
tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
|
||||||
|
} catch let error {
|
||||||
|
parseError = error
|
||||||
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if tunnelConfiguration == nil {
|
if parseError != nil {
|
||||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
if let parseError = parseError as? WireGuardAppError {
|
||||||
|
lastFileImportErrorText = parseError.alertText
|
||||||
|
} else {
|
||||||
|
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
configs.append(tunnelConfiguration)
|
configs.append(tunnelConfiguration)
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -109,9 +109,9 @@ class TunnelViewModel {
|
|||||||
scratchpad[field] = stringValue
|
scratchpad[field] = stringValue
|
||||||
}
|
}
|
||||||
if field == .privateKey {
|
if field == .privateKey {
|
||||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
if stringValue.count == TunnelViewModel.keyLengthInBase64,
|
||||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? ""
|
let privateKey = PrivateKey(base64Key: stringValue) {
|
||||||
scratchpad[.publicKey] = publicKey
|
scratchpad[.publicKey] = privateKey.publicKey.base64Key
|
||||||
} else {
|
} else {
|
||||||
scratchpad.removeValue(forKey: .publicKey)
|
scratchpad.removeValue(forKey: .publicKey)
|
||||||
}
|
}
|
||||||
@@ -128,8 +128,8 @@ class TunnelViewModel {
|
|||||||
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
|
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
|
||||||
var scratchpad = [InterfaceField: String]()
|
var scratchpad = [InterfaceField: String]()
|
||||||
scratchpad[.name] = name
|
scratchpad[.name] = name
|
||||||
scratchpad[.privateKey] = config.privateKey.base64Key() ?? ""
|
scratchpad[.privateKey] = config.privateKey.base64Key
|
||||||
scratchpad[.publicKey] = config.publicKey.base64Key() ?? ""
|
scratchpad[.publicKey] = config.privateKey.publicKey.base64Key
|
||||||
if !config.addresses.isEmpty {
|
if !config.addresses.isEmpty {
|
||||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||||
}
|
}
|
||||||
@@ -139,8 +139,10 @@ class TunnelViewModel {
|
|||||||
if let mtu = config.mtu {
|
if let mtu = config.mtu {
|
||||||
scratchpad[.mtu] = String(mtu)
|
scratchpad[.mtu] = String(mtu)
|
||||||
}
|
}
|
||||||
if !config.dns.isEmpty {
|
if !config.dns.isEmpty || !config.dnsSearch.isEmpty {
|
||||||
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
var dns = config.dns.map { $0.stringRepresentation }
|
||||||
|
dns.append(contentsOf: config.dnsSearch)
|
||||||
|
scratchpad[.dns] = dns.joined(separator: ", ")
|
||||||
}
|
}
|
||||||
return scratchpad
|
return scratchpad
|
||||||
}
|
}
|
||||||
@@ -158,7 +160,7 @@ class TunnelViewModel {
|
|||||||
fieldsWithError.insert(.privateKey)
|
fieldsWithError.insert(.privateKey)
|
||||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
|
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
|
||||||
}
|
}
|
||||||
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
|
guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
|
||||||
fieldsWithError.insert(.privateKey)
|
fieldsWithError.insert(.privateKey)
|
||||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
|
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
|
||||||
}
|
}
|
||||||
@@ -194,15 +196,16 @@ class TunnelViewModel {
|
|||||||
}
|
}
|
||||||
if let dnsString = scratchpad[.dns] {
|
if let dnsString = scratchpad[.dns] {
|
||||||
var dnsServers = [DNSServer]()
|
var dnsServers = [DNSServer]()
|
||||||
|
var dnsSearch = [String]()
|
||||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||||
if let dnsServer = DNSServer(from: dnsServerString) {
|
if let dnsServer = DNSServer(from: dnsServerString) {
|
||||||
dnsServers.append(dnsServer)
|
dnsServers.append(dnsServer)
|
||||||
} else {
|
} else {
|
||||||
fieldsWithError.insert(.dns)
|
dnsSearch.append(dnsServerString)
|
||||||
errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.dns = dnsServers
|
config.dns = dnsServers
|
||||||
|
config.dnsSearch = dnsSearch
|
||||||
}
|
}
|
||||||
|
|
||||||
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
||||||
@@ -251,12 +254,12 @@ class TunnelViewModel {
|
|||||||
var scratchpad = [PeerField: String]()
|
var scratchpad = [PeerField: String]()
|
||||||
var fieldsWithError = Set<PeerField>()
|
var fieldsWithError = Set<PeerField>()
|
||||||
var validatedConfiguration: PeerConfiguration?
|
var validatedConfiguration: PeerConfiguration?
|
||||||
var publicKey: Data? {
|
var publicKey: PublicKey? {
|
||||||
if let validatedConfiguration = validatedConfiguration {
|
if let validatedConfiguration = validatedConfiguration {
|
||||||
return validatedConfiguration.publicKey
|
return validatedConfiguration.publicKey
|
||||||
}
|
}
|
||||||
if let scratchPadPublicKey = scratchpad[.publicKey] {
|
if let scratchPadPublicKey = scratchpad[.publicKey] {
|
||||||
return Data(base64Key: scratchPadPublicKey)
|
return PublicKey(base64Key: scratchPadPublicKey)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -301,10 +304,8 @@ class TunnelViewModel {
|
|||||||
|
|
||||||
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
|
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
|
||||||
var scratchpad = [PeerField: String]()
|
var scratchpad = [PeerField: String]()
|
||||||
if let publicKey = config.publicKey.base64Key() {
|
scratchpad[.publicKey] = config.publicKey.base64Key
|
||||||
scratchpad[.publicKey] = publicKey
|
if let preSharedKey = config.preSharedKey?.base64Key {
|
||||||
}
|
|
||||||
if let preSharedKey = config.preSharedKey?.base64Key() {
|
|
||||||
scratchpad[.preSharedKey] = preSharedKey
|
scratchpad[.preSharedKey] = preSharedKey
|
||||||
}
|
}
|
||||||
if !config.allowedIPs.isEmpty {
|
if !config.allowedIPs.isEmpty {
|
||||||
@@ -337,14 +338,14 @@ class TunnelViewModel {
|
|||||||
fieldsWithError.insert(.publicKey)
|
fieldsWithError.insert(.publicKey)
|
||||||
return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
|
return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
|
||||||
}
|
}
|
||||||
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
|
guard let publicKey = PublicKey(base64Key: publicKeyString) else {
|
||||||
fieldsWithError.insert(.publicKey)
|
fieldsWithError.insert(.publicKey)
|
||||||
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
|
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
|
||||||
}
|
}
|
||||||
var config = PeerConfiguration(publicKey: publicKey)
|
var config = PeerConfiguration(publicKey: publicKey)
|
||||||
var errorMessages = [String]()
|
var errorMessages = [String]()
|
||||||
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
||||||
if let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
if let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) {
|
||||||
config.preSharedKey = preSharedKey
|
config.preSharedKey = preSharedKey
|
||||||
} else {
|
} else {
|
||||||
fieldsWithError.insert(.preSharedKey)
|
fieldsWithError.insert(.preSharedKey)
|
||||||
@@ -397,12 +398,13 @@ class TunnelViewModel {
|
|||||||
|
|
||||||
static let ipv4DefaultRouteString = "0.0.0.0/0"
|
static let ipv4DefaultRouteString = "0.0.0.0/0"
|
||||||
static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs
|
static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs
|
||||||
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
|
"1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8", "4.0.0.0/6", "8.0.0.0/7", "11.0.0.0/8",
|
||||||
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
|
"12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3",
|
||||||
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
|
"160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10",
|
||||||
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
|
"172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9",
|
||||||
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
|
"192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15",
|
||||||
"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"
|
"192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "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) {
|
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
|
||||||
@@ -559,7 +561,7 @@ class TunnelViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||||
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
|
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import os.log
|
import os.log
|
||||||
@@ -21,7 +21,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||||
window.backgroundColor = .white
|
|
||||||
self.window = window
|
self.window = window
|
||||||
|
|
||||||
let mainVC = MainViewController()
|
let mainVC = MainViewController()
|
||||||
@@ -34,10 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||||
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
|
mainVC?.importFromDisposableFile(url: url)
|
||||||
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
|
|
||||||
_ = FileManager.deleteFile(at: url)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 |
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import os.log
|
import os.log
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ extension UITableView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
|
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
|
||||||
//swiftlint:disable:next force_cast
|
// swiftlint:disable:next force_cast
|
||||||
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
|
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ class ButtonCell: UITableViewCell {
|
|||||||
set(value) { button.setTitle(value, for: .normal) }
|
set(value) { button.setTitle(value, for: .normal) }
|
||||||
}
|
}
|
||||||
var hasDestructiveAction: Bool {
|
var hasDestructiveAction: Bool {
|
||||||
get { return button.tintColor == .red }
|
get { return button.tintColor == .systemRed }
|
||||||
set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
|
set(value) { button.tintColor = value ? .systemRed : buttonStandardTintColor }
|
||||||
}
|
}
|
||||||
var onTapped: (() -> Void)?
|
var onTapped: (() -> Void)?
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -9,6 +9,11 @@ class EditableTextCell: UITableViewCell {
|
|||||||
set(value) { valueTextField.text = value }
|
set(value) { valueTextField.text = value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var placeholder: String? {
|
||||||
|
get { return valueTextField.placeholder }
|
||||||
|
set(value) { valueTextField.placeholder = value }
|
||||||
|
}
|
||||||
|
|
||||||
let valueTextField: UITextField = {
|
let valueTextField: UITextField = {
|
||||||
let valueTextField = UITextField()
|
let valueTextField = UITextField()
|
||||||
valueTextField.textAlignment = .left
|
valueTextField.textAlignment = .left
|
||||||
@@ -29,12 +34,13 @@ class EditableTextCell: UITableViewCell {
|
|||||||
valueTextField.delegate = self
|
valueTextField.delegate = self
|
||||||
contentView.addSubview(valueTextField)
|
contentView.addSubview(valueTextField)
|
||||||
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1)
|
// Reduce the bottom margin by 0.5pt to maintain the default cell height (44pt)
|
||||||
|
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: valueTextField.bottomAnchor, constant: -0.5)
|
||||||
bottomAnchorConstraint.priority = .defaultLow
|
bottomAnchorConstraint.priority = .defaultLow
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
valueTextField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
|
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: valueTextField.trailingAnchor),
|
||||||
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: valueTextField.topAnchor),
|
||||||
bottomAnchorConstraint
|
bottomAnchorConstraint
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
@@ -50,6 +56,7 @@ class EditableTextCell: UITableViewCell {
|
|||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
message = ""
|
message = ""
|
||||||
|
placeholder = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -9,7 +9,11 @@ class KeyValueCell: UITableViewCell {
|
|||||||
let keyLabel = UILabel()
|
let keyLabel = UILabel()
|
||||||
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
keyLabel.adjustsFontForContentSizeCategory = true
|
keyLabel.adjustsFontForContentSizeCategory = true
|
||||||
keyLabel.textColor = .black
|
if #available(iOS 13.0, *) {
|
||||||
|
keyLabel.textColor = .label
|
||||||
|
} else {
|
||||||
|
keyLabel.textColor = .black
|
||||||
|
}
|
||||||
keyLabel.textAlignment = .left
|
keyLabel.textAlignment = .left
|
||||||
return keyLabel
|
return keyLabel
|
||||||
}()
|
}()
|
||||||
@@ -23,7 +27,7 @@ class KeyValueCell: UITableViewCell {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
let valueTextField: UITextField = {
|
let valueTextField: UITextField = {
|
||||||
let valueTextField = UITextField()
|
let valueTextField = KeyValueCellTextField()
|
||||||
valueTextField.textAlignment = .right
|
valueTextField.textAlignment = .right
|
||||||
valueTextField.isEnabled = false
|
valueTextField.isEnabled = false
|
||||||
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
@@ -31,7 +35,11 @@ class KeyValueCell: UITableViewCell {
|
|||||||
valueTextField.autocapitalizationType = .none
|
valueTextField.autocapitalizationType = .none
|
||||||
valueTextField.autocorrectionType = .no
|
valueTextField.autocorrectionType = .no
|
||||||
valueTextField.spellCheckingType = .no
|
valueTextField.spellCheckingType = .no
|
||||||
valueTextField.textColor = .gray
|
if #available(iOS 13.0, *) {
|
||||||
|
valueTextField.textColor = .secondaryLabel
|
||||||
|
} else {
|
||||||
|
valueTextField.textColor = .gray
|
||||||
|
}
|
||||||
return valueTextField
|
return valueTextField
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -56,10 +64,18 @@ class KeyValueCell: UITableViewCell {
|
|||||||
|
|
||||||
var isValueValid = true {
|
var isValueValid = true {
|
||||||
didSet {
|
didSet {
|
||||||
if isValueValid {
|
if #available(iOS 13.0, *) {
|
||||||
keyLabel.textColor = .black
|
if isValueValid {
|
||||||
|
keyLabel.textColor = .label
|
||||||
|
} else {
|
||||||
|
keyLabel.textColor = .systemRed
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
keyLabel.textColor = .red
|
if isValueValid {
|
||||||
|
keyLabel.textColor = .black
|
||||||
|
} else {
|
||||||
|
keyLabel.textColor = .red
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +115,6 @@ class KeyValueCell: UITableViewCell {
|
|||||||
expandToFitValueLabelConstraint.priority = .defaultLow + 1
|
expandToFitValueLabelConstraint.priority = .defaultLow + 1
|
||||||
expandToFitValueLabelConstraint.isActive = true
|
expandToFitValueLabelConstraint.isActive = true
|
||||||
|
|
||||||
contentView.addSubview(valueLabelScrollView)
|
|
||||||
|
|
||||||
contentView.addSubview(valueLabelScrollView)
|
contentView.addSubview(valueLabelScrollView)
|
||||||
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
|
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
@@ -218,3 +232,10 @@ extension KeyValueCell: UITextFieldDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class KeyValueCellTextField: UITextField {
|
||||||
|
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
|
||||||
|
// UIKit renders the placeholder label 0.5pt higher
|
||||||
|
return super.placeholderRect(forBounds: bounds).integral.offsetBy(dx: 0, dy: -0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -16,13 +16,19 @@ class SwitchCell: UITableViewCell {
|
|||||||
get { return switchView.isEnabled }
|
get { return switchView.isEnabled }
|
||||||
set(value) {
|
set(value) {
|
||||||
switchView.isEnabled = value
|
switchView.isEnabled = value
|
||||||
textLabel?.textColor = value ? .black : .gray
|
if #available(iOS 13.0, *) {
|
||||||
|
textLabel?.textColor = value ? .label : .secondaryLabel
|
||||||
|
} else {
|
||||||
|
textLabel?.textColor = value ? .black : .gray
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var onSwitchToggled: ((Bool) -> Void)?
|
var onSwitchToggled: ((Bool) -> Void)?
|
||||||
|
|
||||||
var observationToken: AnyObject?
|
var statusObservationToken: AnyObject?
|
||||||
|
var isOnDemandEnabledObservationToken: AnyObject?
|
||||||
|
var hasOnDemandRulesObservationToken: AnyObject?
|
||||||
|
|
||||||
let switchView = UISwitch()
|
let switchView = UISwitch()
|
||||||
|
|
||||||
@@ -47,6 +53,8 @@ class SwitchCell: UITableViewCell {
|
|||||||
isEnabled = true
|
isEnabled = true
|
||||||
message = ""
|
message = ""
|
||||||
isOn = false
|
isOn = false
|
||||||
observationToken = nil
|
statusObservationToken = nil
|
||||||
|
isOnDemandEnabledObservationToken = nil
|
||||||
|
hasOnDemandRulesObservationToken = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -28,7 +28,11 @@ class TextCell: UITableViewCell {
|
|||||||
override func prepareForReuse() {
|
override func prepareForReuse() {
|
||||||
super.prepareForReuse()
|
super.prepareForReuse()
|
||||||
message = ""
|
message = ""
|
||||||
setTextColor(.black)
|
if #available(iOS 13.0, *) {
|
||||||
|
setTextColor(.label)
|
||||||
|
} else {
|
||||||
|
setTextColor(.black)
|
||||||
|
}
|
||||||
setTextAlignment(.left)
|
setTextAlignment(.left)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -30,7 +30,11 @@ class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
|
|||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
||||||
copyableGesture = false
|
copyableGesture = false
|
||||||
valueTextField.textColor = .black
|
if #available(iOS 13.0, *) {
|
||||||
|
valueTextField.textColor = .label
|
||||||
|
} else {
|
||||||
|
valueTextField.textColor = .black
|
||||||
|
}
|
||||||
valueTextField.isEnabled = true
|
valueTextField.isEnabled = true
|
||||||
valueLabelScrollView.isScrollEnabled = false
|
valueLabelScrollView.isScrollEnabled = false
|
||||||
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
|
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class TunnelListCell: UITableViewCell {
|
||||||
|
var tunnel: TunnelContainer? {
|
||||||
|
didSet {
|
||||||
|
// Bind to the tunnel's name
|
||||||
|
nameLabel.text = tunnel?.name ?? ""
|
||||||
|
nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
|
||||||
|
self?.nameLabel.text = tunnel.name
|
||||||
|
}
|
||||||
|
// Bind to the tunnel's status
|
||||||
|
update(from: tunnel, animated: false)
|
||||||
|
statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
|
||||||
|
self?.update(from: tunnel, animated: true)
|
||||||
|
}
|
||||||
|
// Bind to tunnel's on-demand settings
|
||||||
|
isOnDemandEnabledObservationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
|
||||||
|
self?.update(from: tunnel, animated: true)
|
||||||
|
}
|
||||||
|
hasOnDemandRulesObservationToken = tunnel?.observe(\.hasOnDemandRules) { [weak self] tunnel, _ in
|
||||||
|
self?.update(from: tunnel, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var onSwitchToggled: ((Bool) -> Void)?
|
||||||
|
|
||||||
|
let nameLabel: UILabel = {
|
||||||
|
let nameLabel = UILabel()
|
||||||
|
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
nameLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
nameLabel.numberOfLines = 0
|
||||||
|
return nameLabel
|
||||||
|
}()
|
||||||
|
|
||||||
|
let onDemandLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = ""
|
||||||
|
label.font = UIFont.preferredFont(forTextStyle: .caption2)
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
label.numberOfLines = 1
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
} else {
|
||||||
|
label.textColor = .gray
|
||||||
|
}
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
let busyIndicator: UIActivityIndicatorView = {
|
||||||
|
let busyIndicator: UIActivityIndicatorView
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
busyIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
} else {
|
||||||
|
busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||||
|
}
|
||||||
|
busyIndicator.hidesWhenStopped = true
|
||||||
|
return busyIndicator
|
||||||
|
}()
|
||||||
|
|
||||||
|
let statusSwitch = UISwitch()
|
||||||
|
|
||||||
|
private var nameObservationToken: NSKeyValueObservation?
|
||||||
|
private var statusObservationToken: NSKeyValueObservation?
|
||||||
|
private var isOnDemandEnabledObservationToken: NSKeyValueObservation?
|
||||||
|
private var hasOnDemandRulesObservationToken: NSKeyValueObservation?
|
||||||
|
|
||||||
|
private var subTitleLabelBottomConstraint: NSLayoutConstraint?
|
||||||
|
private var nameLabelBottomConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
||||||
|
accessoryType = .disclosureIndicator
|
||||||
|
|
||||||
|
for subview in [statusSwitch, busyIndicator, onDemandLabel, nameLabel] {
|
||||||
|
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(subview)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
|
onDemandLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||||
|
|
||||||
|
let nameLabelBottomConstraint =
|
||||||
|
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
|
||||||
|
nameLabelBottomConstraint.priority = .defaultLow
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
statusSwitch.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||||
|
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1),
|
||||||
|
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: onDemandLabel.trailingAnchor, multiplier: 1),
|
||||||
|
|
||||||
|
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
||||||
|
nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
||||||
|
nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: statusSwitch.leadingAnchor),
|
||||||
|
nameLabelBottomConstraint,
|
||||||
|
|
||||||
|
onDemandLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
onDemandLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1),
|
||||||
|
|
||||||
|
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
busyIndicator.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1)
|
||||||
|
])
|
||||||
|
|
||||||
|
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
reset(animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setEditing(_ editing: Bool, animated: Bool) {
|
||||||
|
super.setEditing(editing, animated: animated)
|
||||||
|
statusSwitch.isEnabled = !editing
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func switchToggled() {
|
||||||
|
onSwitchToggled?(statusSwitch.isOn)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(from tunnel: TunnelContainer?, animated: Bool) {
|
||||||
|
guard let tunnel = tunnel else {
|
||||||
|
reset(animated: animated)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let status = tunnel.status
|
||||||
|
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
|
||||||
|
|
||||||
|
let shouldSwitchBeOn = ((status != .deactivating && status != .inactive) || isOnDemandEngaged)
|
||||||
|
statusSwitch.setOn(shouldSwitchBeOn, animated: true)
|
||||||
|
|
||||||
|
if isOnDemandEngaged && !(status == .activating || status == .active) {
|
||||||
|
statusSwitch.onTintColor = UIColor.systemYellow
|
||||||
|
} else {
|
||||||
|
statusSwitch.onTintColor = UIColor.systemGreen
|
||||||
|
}
|
||||||
|
|
||||||
|
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||||
|
|
||||||
|
if tunnel.hasOnDemandRules {
|
||||||
|
onDemandLabel.text = isOnDemandEngaged ? tr("tunnelListCaptionOnDemand") : ""
|
||||||
|
busyIndicator.stopAnimating()
|
||||||
|
statusSwitch.isUserInteractionEnabled = true
|
||||||
|
} else {
|
||||||
|
onDemandLabel.text = ""
|
||||||
|
if status == .inactive || status == .active {
|
||||||
|
busyIndicator.stopAnimating()
|
||||||
|
} else {
|
||||||
|
busyIndicator.startAnimating()
|
||||||
|
}
|
||||||
|
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reset(animated: Bool) {
|
||||||
|
statusSwitch.thumbTintColor = nil
|
||||||
|
statusSwitch.setOn(false, animated: animated)
|
||||||
|
statusSwitch.isUserInteractionEnabled = false
|
||||||
|
busyIndicator.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -15,9 +15,15 @@ class LogViewController: UIViewController {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
let busyIndicator: UIActivityIndicatorView = {
|
let busyIndicator: UIActivityIndicatorView = {
|
||||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
if #available(iOS 13.0, *) {
|
||||||
busyIndicator.hidesWhenStopped = true
|
let busyIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
return busyIndicator
|
busyIndicator.hidesWhenStopped = true
|
||||||
|
return busyIndicator
|
||||||
|
} else {
|
||||||
|
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||||
|
busyIndicator.hidesWhenStopped = true
|
||||||
|
return busyIndicator
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let paragraphStyle: NSParagraphStyle = {
|
let paragraphStyle: NSParagraphStyle = {
|
||||||
@@ -35,7 +41,11 @@ class LogViewController: UIViewController {
|
|||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = UIView()
|
view = UIView()
|
||||||
view.backgroundColor = .white
|
if #available(iOS 13.0, *) {
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
} else {
|
||||||
|
view.backgroundColor = .white
|
||||||
|
}
|
||||||
|
|
||||||
view.addSubview(textView)
|
view.addSubview(textView)
|
||||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
@@ -81,12 +91,18 @@ class LogViewController: UIViewController {
|
|||||||
let richText = NSMutableAttributedString()
|
let richText = NSMutableAttributedString()
|
||||||
let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
|
let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
|
||||||
let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
|
let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
|
||||||
let lightGrayColor = UIColor(white: 0.88, alpha: 1.0)
|
|
||||||
|
|
||||||
for logEntry in fetchedLogEntries {
|
for logEntry in fetchedLogEntries {
|
||||||
let bgColor = self.isNextLineHighlighted ? lightGrayColor : UIColor.white
|
var bgColor: UIColor
|
||||||
let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
|
var fgColor: UIColor
|
||||||
let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
|
if #available(iOS 13.0, *) {
|
||||||
|
bgColor = self.isNextLineHighlighted ? .systemGray3 : .systemBackground
|
||||||
|
fgColor = .label
|
||||||
|
} else {
|
||||||
|
bgColor = self.isNextLineHighlighted ? UIColor(white: 0.88, alpha: 1.0) : UIColor.white
|
||||||
|
fgColor = .black
|
||||||
|
}
|
||||||
|
let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
|
||||||
|
let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
|
||||||
richText.append(timestampText)
|
richText.append(timestampText)
|
||||||
richText.append(messageText)
|
richText.append(messageText)
|
||||||
self.isNextLineHighlighted.toggle()
|
self.isNextLineHighlighted.toggle()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -11,7 +11,11 @@ class MainViewController: UISplitViewController {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
let detailVC = UIViewController()
|
let detailVC = UIViewController()
|
||||||
detailVC.view.backgroundColor = .white
|
if #available(iOS 13.0, *) {
|
||||||
|
detailVC.view.backgroundColor = .systemBackground
|
||||||
|
} else {
|
||||||
|
detailVC.view.backgroundColor = .white
|
||||||
|
}
|
||||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||||
|
|
||||||
let masterVC = TunnelsListTableViewController()
|
let masterVC = TunnelsListTableViewController()
|
||||||
@@ -109,6 +113,19 @@ extension MainViewController {
|
|||||||
onTunnelsManagerReady = showTunnelDetailBlock
|
onTunnelsManagerReady = showTunnelDetailBlock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func importFromDisposableFile(url: URL) {
|
||||||
|
let importFromFileBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
|
||||||
|
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) {
|
||||||
|
_ = FileManager.deleteFile(at: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let tunnelsManager = tunnelsManager {
|
||||||
|
importFromFileBlock(tunnelsManager)
|
||||||
|
} else {
|
||||||
|
onTunnelsManagerReady = importFromFileBlock
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainViewController: UISplitViewControllerDelegate {
|
extension MainViewController: UISplitViewControllerDelegate {
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol QRScanViewControllerDelegate: class {
|
protocol QRScanViewControllerDelegate: AnyObject {
|
||||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?)
|
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SystemConfiguration.CaptiveNetwork
|
import SystemConfiguration.CaptiveNetwork
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
protocol SSIDOptionEditTableViewControllerDelegate: class {
|
protocol SSIDOptionEditTableViewControllerDelegate: AnyObject {
|
||||||
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
|
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,9 +40,16 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
|||||||
selectedOption = option
|
selectedOption = option
|
||||||
selectedSSIDs = ssids
|
selectedSSIDs = ssids
|
||||||
super.init(style: .grouped)
|
super.init(style: .grouped)
|
||||||
connectedSSID = getConnectedSSID()
|
|
||||||
loadSections()
|
loadSections()
|
||||||
loadAddSSIDRows()
|
addSSIDRows.removeAll()
|
||||||
|
addSSIDRows.append(.addNewSSID)
|
||||||
|
|
||||||
|
getConnectedSSID { [weak self] ssid in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.connectedSSID = ssid
|
||||||
|
self.updateCurrentSSIDEntry()
|
||||||
|
self.updateTableViewAddSSIDRows()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
@@ -60,6 +68,7 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
|||||||
tableView.register(TextCell.self)
|
tableView.register(TextCell.self)
|
||||||
tableView.isEditing = true
|
tableView.isEditing = true
|
||||||
tableView.allowsSelectionDuringEditing = true
|
tableView.allowsSelectionDuringEditing = true
|
||||||
|
tableView.keyboardDismissMode = .onDrag
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSections() {
|
func loadSections() {
|
||||||
@@ -71,14 +80,14 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAddSSIDRows() {
|
func updateCurrentSSIDEntry() {
|
||||||
addSSIDRows.removeAll()
|
if let connectedSSID = connectedSSID, !selectedSSIDs.contains(connectedSSID) {
|
||||||
if let connectedSSID = connectedSSID {
|
if let first = addSSIDRows.first, case .addNewSSID = first {
|
||||||
if !selectedSSIDs.contains(connectedSSID) {
|
addSSIDRows.insert(.addConnectedSSID(connectedSSID: connectedSSID), at: 0)
|
||||||
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
|
|
||||||
}
|
}
|
||||||
|
} else if let first = addSSIDRows.first, case .addConnectedSSID = first {
|
||||||
|
addSSIDRows.removeFirst()
|
||||||
}
|
}
|
||||||
addSSIDRows.append(.addNewSSID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTableViewAddSSIDRows() {
|
func updateTableViewAddSSIDRows() {
|
||||||
@@ -176,7 +185,11 @@ extension SSIDOptionEditTableViewController {
|
|||||||
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.message = tr("tunnelOnDemandNoSSIDs")
|
cell.message = tr("tunnelOnDemandNoSSIDs")
|
||||||
cell.setTextColor(.gray)
|
if #available(iOS 13.0, *) {
|
||||||
|
cell.setTextColor(.secondaryLabel)
|
||||||
|
} else {
|
||||||
|
cell.setTextColor(.gray)
|
||||||
|
}
|
||||||
cell.setTextAlignment(.center)
|
cell.setTextAlignment(.center)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
@@ -184,12 +197,13 @@ extension SSIDOptionEditTableViewController {
|
|||||||
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.message = selectedSSIDs[indexPath.row]
|
cell.message = selectedSSIDs[indexPath.row]
|
||||||
|
cell.placeholder = tr("tunnelOnDemandSSIDTextFieldPlaceholder")
|
||||||
cell.isEditing = true
|
cell.isEditing = true
|
||||||
cell.onValueBeingEdited = { [weak self, weak cell] text in
|
cell.onValueBeingEdited = { [weak self, weak cell] text in
|
||||||
guard let self = self, let cell = cell else { return }
|
guard let self = self, let cell = cell else { return }
|
||||||
if let row = self.tableView.indexPath(for: cell)?.row {
|
if let row = self.tableView.indexPath(for: cell)?.row {
|
||||||
self.selectedSSIDs[row] = text
|
self.selectedSSIDs[row] = text
|
||||||
self.loadAddSSIDRows()
|
self.updateCurrentSSIDEntry()
|
||||||
self.updateTableViewAddSSIDRows()
|
self.updateTableViewAddSSIDRows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +234,7 @@ extension SSIDOptionEditTableViewController {
|
|||||||
} else {
|
} else {
|
||||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||||
}
|
}
|
||||||
loadAddSSIDRows()
|
updateCurrentSSIDEntry()
|
||||||
updateTableViewAddSSIDRows()
|
updateTableViewAddSSIDRows()
|
||||||
case .addSSIDs:
|
case .addSSIDs:
|
||||||
assert(editingStyle == .insert)
|
assert(editingStyle == .insert)
|
||||||
@@ -240,7 +254,7 @@ extension SSIDOptionEditTableViewController {
|
|||||||
} else {
|
} else {
|
||||||
tableView.insertRows(at: [indexPath], with: .automatic)
|
tableView.insertRows(at: [indexPath], with: .automatic)
|
||||||
}
|
}
|
||||||
loadAddSSIDRows()
|
updateCurrentSSIDEntry()
|
||||||
updateTableViewAddSSIDRows()
|
updateTableViewAddSSIDRows()
|
||||||
if newSSID.isEmpty {
|
if newSSID.isEmpty {
|
||||||
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
|
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
|
||||||
@@ -249,6 +263,31 @@ extension SSIDOptionEditTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func getConnectedSSID(completionHandler: @escaping (String?) -> Void) {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
completionHandler("Simulator Wi-Fi")
|
||||||
|
#else
|
||||||
|
if #available(iOS 14, *) {
|
||||||
|
NEHotspotNetwork.fetchCurrent { hotspotNetwork in
|
||||||
|
completionHandler(hotspotNetwork?.ssid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] {
|
||||||
|
for interface in supportedInterfaces {
|
||||||
|
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
|
||||||
|
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
|
||||||
|
completionHandler(!ssid.isEmpty ? ssid : nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler(nil)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SSIDOptionEditTableViewController {
|
extension SSIDOptionEditTableViewController {
|
||||||
@@ -266,6 +305,10 @@ extension SSIDOptionEditTableViewController {
|
|||||||
case .ssidOption:
|
case .ssidOption:
|
||||||
let previousOption = selectedOption
|
let previousOption = selectedOption
|
||||||
selectedOption = ssidOptionFields[indexPath.row]
|
selectedOption = ssidOptionFields[indexPath.row]
|
||||||
|
guard previousOption != selectedOption else {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
loadSections()
|
loadSections()
|
||||||
if previousOption == .anySSID {
|
if previousOption == .anySSID {
|
||||||
let indexSet = IndexSet(1 ... 2)
|
let indexSet = IndexSet(1 ... 2)
|
||||||
@@ -281,15 +324,3 @@ extension SSIDOptionEditTableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import os.log
|
import os.log
|
||||||
@@ -144,8 +144,8 @@ extension SettingsTableViewController {
|
|||||||
cell.copyableGesture = false
|
cell.copyableGesture = false
|
||||||
cell.key = field.localizedUIString
|
cell.key = field.localizedUIString
|
||||||
if field == .iosAppVersion {
|
if field == .iosAppVersion {
|
||||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
var appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown version"
|
||||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
if let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
|
||||||
appVersion += " (\(appBuild))"
|
appVersion += " (\(appBuild))"
|
||||||
}
|
}
|
||||||
cell.value = appVersion
|
cell.value = appVersion
|
||||||
@@ -160,8 +160,7 @@ extension SettingsTableViewController {
|
|||||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
} else {
|
} else if field == .viewLog {
|
||||||
assert(field == .viewLog)
|
|
||||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.buttonText = field.localizedUIString
|
cell.buttonText = field.localizedUIString
|
||||||
cell.onTapped = { [weak self] in
|
cell.onTapped = { [weak self] in
|
||||||
@@ -169,5 +168,6 @@ extension SettingsTableViewController {
|
|||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
fatalError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class TunnelDetailTableViewController: UITableViewController {
|
|||||||
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
|
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
|
||||||
editVC.delegate = self
|
editVC.delegate = self
|
||||||
let editNC = UINavigationController(rootViewController: editVC)
|
let editNC = UINavigationController(rootViewController: editVC)
|
||||||
editNC.modalPresentationStyle = .formSheet
|
editNC.modalPresentationStyle = .fullScreen
|
||||||
self.present(editNC, animated: true)
|
self.present(editNC, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,8 +152,8 @@ class TunnelDetailTableViewController: UITableViewController {
|
|||||||
}
|
}
|
||||||
}!
|
}!
|
||||||
let firstPeerSectionIndex = interfaceSectionIndex + 1
|
let firstPeerSectionIndex = interfaceSectionIndex + 1
|
||||||
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
let interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
||||||
var peerFieldIsVisible = self.peerFieldIsVisible
|
let peerFieldIsVisible = self.peerFieldIsVisible
|
||||||
|
|
||||||
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
||||||
for (index, field) in fields.enumerated() {
|
for (index, field) in fields.enumerated() {
|
||||||
@@ -324,8 +324,22 @@ extension TunnelDetailTableViewController {
|
|||||||
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
|
||||||
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
|
func update(cell: SwitchCell?, with tunnel: TunnelContainer) {
|
||||||
let text: String
|
guard let cell = cell else { return }
|
||||||
|
|
||||||
|
let status = tunnel.status
|
||||||
|
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
|
||||||
|
|
||||||
|
let isSwitchOn = (status == .activating || status == .active || isOnDemandEngaged)
|
||||||
|
cell.switchView.setOn(isSwitchOn, animated: true)
|
||||||
|
|
||||||
|
if isOnDemandEngaged && !(status == .activating || status == .active) {
|
||||||
|
cell.switchView.onTintColor = UIColor.systemYellow
|
||||||
|
} else {
|
||||||
|
cell.switchView.onTintColor = UIColor.systemGreen
|
||||||
|
}
|
||||||
|
|
||||||
|
var text: String
|
||||||
switch status {
|
switch status {
|
||||||
case .inactive:
|
case .inactive:
|
||||||
text = tr("tunnelStatusInactive")
|
text = tr("tunnelStatusInactive")
|
||||||
@@ -342,26 +356,49 @@ extension TunnelDetailTableViewController {
|
|||||||
case .waiting:
|
case .waiting:
|
||||||
text = tr("tunnelStatusWaiting")
|
text = tr("tunnelStatusWaiting")
|
||||||
}
|
}
|
||||||
cell.textLabel?.text = text
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
|
if tunnel.hasOnDemandRules {
|
||||||
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
|
text += isOnDemandEngaged ? tr("tunnelStatusAddendumOnDemand") : ""
|
||||||
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
|
cell.switchView.isUserInteractionEnabled = true
|
||||||
|
cell.isEnabled = true
|
||||||
|
} else {
|
||||||
|
cell.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||||
|
cell.isEnabled = (status == .inactive || status == .active)
|
||||||
}
|
}
|
||||||
cell.isEnabled = status == .active || status == .inactive
|
|
||||||
|
if tunnel.hasOnDemandRules && !isOnDemandEngaged && status == .inactive {
|
||||||
|
text = tr("tunnelStatusOnDemandDisabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
cell.textLabel?.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
statusUpdate(cell, tunnel.status)
|
update(cell: cell, with: tunnel)
|
||||||
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
cell.statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||||
guard let cell = cell else { return }
|
update(cell: cell, with: tunnel)
|
||||||
statusUpdate(cell, tunnel.status)
|
}
|
||||||
|
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
|
||||||
|
update(cell: cell, with: tunnel)
|
||||||
|
}
|
||||||
|
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules) { [weak cell] tunnel, _ in
|
||||||
|
update(cell: cell, with: tunnel)
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.onSwitchToggled = { [weak self] isOn in
|
cell.onSwitchToggled = { [weak self] isOn in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if isOn {
|
|
||||||
self.tunnelsManager.startActivation(of: self.tunnel)
|
if self.tunnel.hasOnDemandRules {
|
||||||
|
self.tunnelsManager.setOnDemandEnabled(isOn, on: self.tunnel) { error in
|
||||||
|
if error == nil && !isOn {
|
||||||
|
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
if isOn {
|
||||||
|
self.tunnelsManager.startActivation(of: self.tunnel)
|
||||||
|
} else {
|
||||||
|
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cell
|
return cell
|
||||||
@@ -397,6 +434,7 @@ extension TunnelDetailTableViewController {
|
|||||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.key = field.localizedUIString
|
cell.key = field.localizedUIString
|
||||||
cell.value = onDemandViewModel.localizedInterfaceDescription
|
cell.value = onDemandViewModel.localizedInterfaceDescription
|
||||||
|
cell.copyableGesture = false
|
||||||
return cell
|
return cell
|
||||||
} else {
|
} else {
|
||||||
assert(field == .ssid)
|
assert(field == .ssid)
|
||||||
@@ -404,6 +442,7 @@ extension TunnelDetailTableViewController {
|
|||||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
cell.key = field.localizedUIString
|
cell.key = field.localizedUIString
|
||||||
cell.value = onDemandViewModel.ssidOption.localizedUIString
|
cell.value = onDemandViewModel.ssidOption.localizedUIString
|
||||||
|
cell.copyableGesture = false
|
||||||
return cell
|
return cell
|
||||||
} else {
|
} else {
|
||||||
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol TunnelEditTableViewControllerDelegate: class {
|
protocol TunnelEditTableViewControllerDelegate: AnyObject {
|
||||||
func tunnelSaved(tunnel: TunnelContainer)
|
func tunnelSaved(tunnel: TunnelContainer)
|
||||||
func tunnelEditingCancelled()
|
func tunnelEditingCancelled()
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ extension TunnelEditTableViewController {
|
|||||||
cell.onTapped = { [weak self] in
|
cell.onTapped = { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64Key() ?? ""
|
self.tunnelViewModel.interfaceData[.privateKey] = PrivateKey().base64Key
|
||||||
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
|
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
|
||||||
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
||||||
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
|
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
|
||||||
@@ -266,7 +266,7 @@ extension TunnelEditTableViewController {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
|
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
|
||||||
if isAllowedIPsChanged {
|
if isAllowedIPsChanged {
|
||||||
let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } }
|
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) {
|
if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
|
||||||
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ extension TunnelEditTableViewController {
|
|||||||
let removedSectionIndices = self.deletePeer(peer: peerData)
|
let removedSectionIndices = self.deletePeer(peer: peerData)
|
||||||
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||||
|
|
||||||
//swiftlint:disable:next trailing_closure
|
// swiftlint:disable:next trailing_closure
|
||||||
tableView.performBatchUpdates({
|
tableView.performBatchUpdates({
|
||||||
self.tableView.deleteSections(removedSectionIndices, with: .fade)
|
self.tableView.deleteSections(removedSectionIndices, with: .fade)
|
||||||
if shouldShowExcludePrivateIPs {
|
if shouldShowExcludePrivateIPs {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MobileCoreServices
|
import MobileCoreServices
|
||||||
@@ -32,7 +32,12 @@ class TunnelsListTableViewController: UIViewController {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
let busyIndicator: UIActivityIndicatorView = {
|
let busyIndicator: UIActivityIndicatorView = {
|
||||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
let busyIndicator: UIActivityIndicatorView
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
busyIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
} else {
|
||||||
|
busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||||
|
}
|
||||||
busyIndicator.hidesWhenStopped = true
|
busyIndicator.hidesWhenStopped = true
|
||||||
return busyIndicator
|
return busyIndicator
|
||||||
}()
|
}()
|
||||||
@@ -46,7 +51,11 @@ class TunnelsListTableViewController: UIViewController {
|
|||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = UIView()
|
view = UIView()
|
||||||
view.backgroundColor = .white
|
if #available(iOS 13.0, *) {
|
||||||
|
view.backgroundColor = .systemBackground
|
||||||
|
} else {
|
||||||
|
view.backgroundColor = .white
|
||||||
|
}
|
||||||
|
|
||||||
tableView.dataSource = self
|
tableView.dataSource = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
@@ -178,7 +187,7 @@ class TunnelsListTableViewController: UIViewController {
|
|||||||
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
|
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
|
||||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
|
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
|
||||||
let editNC = UINavigationController(rootViewController: editVC)
|
let editNC = UINavigationController(rootViewController: editVC)
|
||||||
editNC.modalPresentationStyle = .formSheet
|
editNC.modalPresentationStyle = .fullScreen
|
||||||
present(editNC, animated: true)
|
present(editNC, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,10 +317,18 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
|||||||
cell.tunnel = tunnel
|
cell.tunnel = tunnel
|
||||||
cell.onSwitchToggled = { [weak self] isOn in
|
cell.onSwitchToggled = { [weak self] isOn in
|
||||||
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
|
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
|
||||||
if isOn {
|
if tunnel.hasOnDemandRules {
|
||||||
tunnelsManager.startActivation(of: tunnel)
|
tunnelsManager.setOnDemandEnabled(isOn, on: tunnel) { error in
|
||||||
|
if error == nil && !isOn {
|
||||||
|
tunnelsManager.startDeactivation(of: tunnel)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tunnelsManager.startDeactivation(of: tunnel)
|
if isOn {
|
||||||
|
tunnelsManager.startActivation(of: tunnel)
|
||||||
|
} else {
|
||||||
|
tunnelsManager.startDeactivation(of: tunnel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,7 +406,11 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
|||||||
(splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false)
|
(splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false)
|
||||||
} else {
|
} else {
|
||||||
let detailVC = UIViewController()
|
let detailVC = UIViewController()
|
||||||
detailVC.view.backgroundColor = .white
|
if #available(iOS 13.0, *) {
|
||||||
|
detailVC.view.backgroundColor = .systemBackground
|
||||||
|
} else {
|
||||||
|
detailVC.view.backgroundColor = .white
|
||||||
|
}
|
||||||
let detailNC = UINavigationController(rootViewController: detailVC)
|
let detailNC = UINavigationController(rootViewController: detailVC)
|
||||||
splitViewController.showDetailViewController(detailNC, sender: self)
|
splitViewController.showDetailViewController(detailNC, sender: self)
|
||||||
}
|
}
|
||||||
@@ -402,12 +423,12 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension UISplitViewController {
|
extension UISplitViewController {
|
||||||
func showDetailViewController(_ vc: UIViewController, sender: Any?, animated: Bool) {
|
func showDetailViewController(_ viewController: UIViewController, sender: Any?, animated: Bool) {
|
||||||
if animated {
|
if animated {
|
||||||
showDetailViewController(vc, sender: sender)
|
showDetailViewController(viewController, sender: sender)
|
||||||
} else {
|
} else {
|
||||||
UIView.performWithoutAnimation {
|
UIView.performWithoutAnimation {
|
||||||
showDetailViewController(vc, sender: sender)
|
showDetailViewController(viewController, sender: sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import ServiceManagement
|
import ServiceManagement
|
||||||
@@ -65,19 +65,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
|
|
||||||
if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
|
|
||||||
if LaunchedAtLoginDetector.isReopenedByLoginItemHelper(reopenAppleEvent: appleEvent) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasVisibleWindows {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
showManageTunnelsWindow(completion: nil)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func confirmAndQuit() {
|
@objc func confirmAndQuit() {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = tr("macConfirmAndQuitAlertMessage")
|
alert.messageText = tr("macConfirmAndQuitAlertMessage")
|
||||||
@@ -132,10 +119,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
if UserDefaults.standard.bool(forKey: "shouldSuppressAppStoreUpdateDetection") {
|
|
||||||
wg_log(.debug, staticMessage: "App Store update detection is suppressed")
|
|
||||||
return .terminateNow
|
|
||||||
}
|
|
||||||
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
|
||||||
return .terminateNow
|
return .terminateNow
|
||||||
}
|
}
|
||||||
@@ -217,7 +200,8 @@ extension AppDelegate {
|
|||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
NSApp.orderFrontStandardAboutPanel(options: [
|
NSApp.orderFrontStandardAboutPanel(options: [
|
||||||
.applicationVersion: appVersionString,
|
.applicationVersion: appVersionString,
|
||||||
.version: ""
|
.version: "",
|
||||||
|
.credits: ""
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Application: NSApplication {
|
class Application: NSApplication {
|
||||||
|
|
||||||
private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate
|
private var appDelegate: AppDelegate? // swiftlint:disable:this weak_delegate
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |