Compare commits
194 Commits
0.0.201906
...
1.0.15-26
Author | SHA1 | Date | |
---|---|---|---|
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 |
6
.gitignore
vendored
@ -31,6 +31,10 @@ xcuserdata
|
||||
*.hmap
|
||||
*.ipa
|
||||
|
||||
# Swift Package Manager
|
||||
.swiftpm
|
||||
.build/
|
||||
|
||||
# Fastlane
|
||||
*.app.dSYM.zip
|
||||
*.mobileprovision
|
||||
@ -41,7 +45,7 @@ Preview.html
|
||||
output
|
||||
|
||||
# Wireguard specific
|
||||
WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
Sources/WireGuardApp/Config/Developer.xcconfig
|
||||
|
||||
# Vim
|
||||
.*.sw*
|
||||
|
@ -7,6 +7,7 @@ disabled_rules:
|
||||
- type_body_length
|
||||
- function_body_length
|
||||
- nesting
|
||||
- inclusive_language
|
||||
opt_in_rules:
|
||||
- empty_count
|
||||
- empty_string
|
2
COPYING
@ -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
|
||||
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.
|
||||
|
||||
[wg-quick(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
|
||||
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8
|
||||
[wg-quick(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
|
||||
[wg(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
|
||||
|
40
Package.swift
Normal file
@ -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")]
|
||||
)
|
||||
]
|
||||
)
|
54
README.md
@ -14,11 +14,11 @@ $ cd wireguard-apple
|
||||
- Rename and populate developer team ID file:
|
||||
|
||||
```
|
||||
$ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
|
||||
$ cp Sources/WireGuardApp/Config/Developer.xcconfig.template Sources/WireGuardApp/Config/Developer.xcconfig
|
||||
$ vim Sources/WireGuardApp/Config/Developer.xcconfig
|
||||
```
|
||||
|
||||
- Install swiftlint and go:
|
||||
- Install swiftlint and go 1.15:
|
||||
|
||||
```
|
||||
$ brew install swiftlint go
|
||||
@ -27,11 +27,57 @@ $ brew install swiftlint go
|
||||
- Open project in Xcode:
|
||||
|
||||
```
|
||||
$ open ./WireGuard/WireGuard.xcodeproj
|
||||
$ open WireGuard.xcodeproj
|
||||
```
|
||||
|
||||
- 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
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
@ -35,6 +35,10 @@ extension FileManager {
|
||||
return sharedFolderURL?.appendingPathComponent("last-error.txt")
|
||||
}
|
||||
|
||||
static var loginHelperTimestampURL: URL? {
|
||||
return sharedFolderURL?.appendingPathComponent("login-helper-timestamp.bin")
|
||||
}
|
||||
|
||||
static func deleteFile(at url: URL) -> Bool {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
@ -7,9 +7,8 @@ import Security
|
||||
class Keychain {
|
||||
static func openReference(called ref: Data) -> String? {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref,
|
||||
kSecReturnData as String: true] as CFDictionary,
|
||||
let ret = SecItemCopyMatching([kSecValuePersistentRef: ref,
|
||||
kSecReturnData: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
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? {
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
if id.hasSuffix(".network-extension") {
|
||||
id.removeLast(".network-extension".count)
|
||||
if bundleIdentifier.hasSuffix(".network-extension") {
|
||||
bundleIdentifier.removeLast(".network-extension".count)
|
||||
}
|
||||
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
|
||||
kSecAttrAccount as String: name + ": " + UUID().uuidString,
|
||||
kSecAttrDescription as String: "wg-quick(8) config",
|
||||
kSecAttrService as String: id,
|
||||
kSecValueData as String: value.data(using: .utf8) as Any,
|
||||
kSecReturnPersistentRef as String: true]
|
||||
let itemLabel = "WireGuard Tunnel: \(name)"
|
||||
var items: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: itemLabel,
|
||||
kSecAttrAccount: name + ": " + UUID().uuidString,
|
||||
kSecAttrDescription: "wg-quick(8) config",
|
||||
kSecAttrService: bundleIdentifier,
|
||||
kSecValueData: value.data(using: .utf8) as Any,
|
||||
kSecReturnPersistentRef: true]
|
||||
|
||||
#if os(iOS)
|
||||
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
items[kSecAttrAccessGroup] = FileManager.appGroupId
|
||||
items[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
|
||||
#elseif os(macOS)
|
||||
items[kSecAttrSynchronizable as String] = false
|
||||
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
||||
items[kSecAttrSynchronizable] = false
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
@ -60,14 +60,12 @@ class Keychain {
|
||||
return nil
|
||||
}
|
||||
var access: SecAccess?
|
||||
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
|
||||
[extensionApp!, mainApp!] as CFArray,
|
||||
&access)
|
||||
ret = SecAccessCreate(itemLabel as CFString, [extensionApp!, mainApp!] as CFArray, &access)
|
||||
if ret != errSecSuccess || access == nil {
|
||||
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
|
||||
return nil
|
||||
}
|
||||
items[kSecAttrAccess as String] = access!
|
||||
items[kSecAttrAccess] = access!
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
@ -85,7 +83,7 @@ class Keychain {
|
||||
}
|
||||
|
||||
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 {
|
||||
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
|
||||
}
|
||||
@ -93,10 +91,10 @@ class Keychain {
|
||||
|
||||
static func deleteReferences(except whitelist: Set<Data>) {
|
||||
var result: CFTypeRef?
|
||||
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnPersistentRef as String: true] as CFDictionary,
|
||||
let ret = SecItemCopyMatching([kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrService: Bundle.main.bundleIdentifier as Any,
|
||||
kSecMatchLimit: kSecMatchLimitAll,
|
||||
kSecReturnPersistentRef: true] as CFDictionary,
|
||||
&result)
|
||||
if ret != errSecSuccess || result == nil {
|
||||
return
|
||||
@ -110,8 +108,7 @@ class Keychain {
|
||||
}
|
||||
|
||||
static func verifyReference(called ref: Data) -> Bool {
|
||||
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
|
||||
kSecValuePersistentRef as String: ref] as CFDictionary,
|
||||
nil) == errSecSuccess
|
||||
return SecItemCopyMatching([kSecValuePersistentRef: ref] as CFDictionary,
|
||||
nil) != errSecItemNotFound
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
@ -49,8 +49,8 @@ public class Logger {
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
let goBackendVersion = WIREGUARD_GO_VERSION
|
||||
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
|
||||
|
||||
Logger.global?.log(message: "App version: \(appVersion)")
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
@ -1,6 +1,6 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef RINGLOGGER_H
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
@ -22,6 +22,9 @@ extension NETunnelProviderProtocol {
|
||||
if passwordReference == nil {
|
||||
return nil
|
||||
}
|
||||
#if os(macOS)
|
||||
providerConfiguration = ["UID": getuid()]
|
||||
#endif
|
||||
|
||||
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
|
||||
if endpoints.count == 1 {
|
||||
@ -60,11 +63,46 @@ extension NETunnelProviderProtocol {
|
||||
* in the keychain. But it's still useful to keep the migration
|
||||
* around so that .mobileconfig files are easier.
|
||||
*/
|
||||
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
|
||||
providerConfiguration = nil
|
||||
guard passwordReference == nil else { return true }
|
||||
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
|
||||
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
|
||||
return true
|
||||
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
|
||||
#if os(macOS)
|
||||
providerConfiguration = ["UID": getuid()]
|
||||
#elseif os(iOS)
|
||||
providerConfiguration = nil
|
||||
#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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -39,7 +39,7 @@ extension TunnelConfiguration {
|
||||
var interfaceConfiguration: InterfaceConfiguration?
|
||||
var peerConfigurations = [PeerConfiguration]()
|
||||
|
||||
let lines = wgQuickConfig.split(separator: "\n")
|
||||
let lines = wgQuickConfig.split { $0.isNewline }
|
||||
|
||||
var parserState = ParserState.notInASection
|
||||
var attributes = [String: String]()
|
||||
@ -111,7 +111,7 @@ extension TunnelConfiguration {
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
throw ParseError.multiplePeersWithSamePublicKey
|
||||
}
|
||||
@ -125,9 +125,7 @@ extension TunnelConfiguration {
|
||||
|
||||
func asWgQuickConfig() -> String {
|
||||
var output = "[Interface]\n"
|
||||
if let privateKey = interface.privateKey.base64Key() {
|
||||
output.append("PrivateKey = \(privateKey)\n")
|
||||
}
|
||||
output.append("PrivateKey = \(interface.privateKey.base64Key)\n")
|
||||
if let listenPort = interface.listenPort {
|
||||
output.append("ListenPort = \(listenPort)\n")
|
||||
}
|
||||
@ -135,8 +133,10 @@ extension TunnelConfiguration {
|
||||
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
output.append("Address = \(addressString)\n")
|
||||
}
|
||||
if !interface.dns.isEmpty {
|
||||
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
|
||||
var dnsLine = interface.dns.map { $0.stringRepresentation }
|
||||
dnsLine.append(contentsOf: interface.dnsSearch)
|
||||
let dnsString = dnsLine.joined(separator: ", ")
|
||||
output.append("DNS = \(dnsString)\n")
|
||||
}
|
||||
if let mtu = interface.mtu {
|
||||
@ -145,10 +145,8 @@ extension TunnelConfiguration {
|
||||
|
||||
for peer in peers {
|
||||
output.append("\n[Peer]\n")
|
||||
if let publicKey = peer.publicKey.base64Key() {
|
||||
output.append("PublicKey = \(publicKey)\n")
|
||||
}
|
||||
if let preSharedKey = peer.preSharedKey?.base64Key() {
|
||||
output.append("PublicKey = \(peer.publicKey.base64Key)\n")
|
||||
if let preSharedKey = peer.preSharedKey?.base64Key {
|
||||
output.append("PresharedKey = \(preSharedKey)\n")
|
||||
}
|
||||
if !peer.allowedIPs.isEmpty {
|
||||
@ -170,7 +168,7 @@ extension TunnelConfiguration {
|
||||
guard let privateKeyString = attributes["privatekey"] else {
|
||||
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)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
@ -192,13 +190,16 @@ extension TunnelConfiguration {
|
||||
}
|
||||
if let dnsString = attributes["dns"] {
|
||||
var dnsServers = [DNSServer]()
|
||||
var dnsSearch = [String]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
guard let dnsServer = DNSServer(from: dnsServerString) else {
|
||||
throw ParseError.interfaceHasInvalidDNS(dnsServerString)
|
||||
if let dnsServer = DNSServer(from: dnsServerString) {
|
||||
dnsServers.append(dnsServer)
|
||||
} else {
|
||||
dnsSearch.append(dnsServerString)
|
||||
}
|
||||
dnsServers.append(dnsServer)
|
||||
}
|
||||
interface.dns = dnsServers
|
||||
interface.dnsSearch = dnsSearch
|
||||
}
|
||||
if let mtuString = attributes["mtu"] {
|
||||
guard let mtu = UInt16(mtuString) else {
|
||||
@ -213,12 +214,12 @@ extension TunnelConfiguration {
|
||||
guard let publicKeyString = attributes["publickey"] else {
|
||||
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)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
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)
|
||||
}
|
||||
peer.preSharedKey = preSharedKey
|
33
Sources/Shared/NotificationToken.swift
Normal file
@ -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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
// iOS permission prompts
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
// Generic alert action names
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"tunnelsListSelectAllButtonTitle" = "Select All";
|
||||
"tunnelsListDeleteButtonTitle" = "Delete";
|
||||
"tunnelsListSelectedTitle (%d)" = "%d selected";
|
||||
"tunnelListCaptionOnDemand" = "On-Demand";
|
||||
|
||||
// Tunnels list menu
|
||||
|
||||
@ -55,6 +56,11 @@
|
||||
"tunnelStatusRestarting" = "Restarting";
|
||||
"tunnelStatusWaiting" = "Waiting";
|
||||
|
||||
"tunnelStatusAddendumOnDemand" = " (On-Demand)";
|
||||
"tunnelStatusOnDemandDisabled" = "On-Demand Disabled";
|
||||
"tunnelStatusAddendumOnDemandEnabled" = ", On-Demand Enabled";
|
||||
"tunnelStatusAddendumOnDemandDisabled" = ", On-Demand Disabled";
|
||||
|
||||
"macToggleStatusButtonActivate" = "Activate";
|
||||
"macToggleStatusButtonActivating" = "Activating…";
|
||||
"macToggleStatusButtonDeactivate" = "Deactivate";
|
||||
@ -62,6 +68,9 @@
|
||||
"macToggleStatusButtonReasserting" = "Reactivating…";
|
||||
"macToggleStatusButtonRestarting" = "Restarting…";
|
||||
"macToggleStatusButtonWaiting" = "Waiting…";
|
||||
"macToggleStatusButtonEnableOnDemand" = "Enable On-Demand";
|
||||
"macToggleStatusButtonDisableOnDemand" = "Disable On-Demand";
|
||||
"macToggleStatusButtonDisableOnDemandDeactivate" = "Disable On-Demand and Deactivate";
|
||||
|
||||
"tunnelSectionTitleInterface" = "Interface";
|
||||
|
||||
@ -109,8 +118,9 @@
|
||||
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
|
||||
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
|
||||
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
|
||||
"tunnelOnDemandSSIDTextFieldPlaceholder" = "SSID";
|
||||
|
||||
"tunnelOnDemandKey" = "On demand";
|
||||
"tunnelOnDemandKey" = "On-demand";
|
||||
"tunnelOnDemandOptionOff" = "Off";
|
||||
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
|
||||
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
|
||||
@ -255,8 +265,6 @@
|
||||
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
|
||||
"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";
|
||||
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
|
||||
|
||||
@ -297,6 +305,7 @@
|
||||
"macMenuNetworksNone" = "Networks: None";
|
||||
|
||||
"macMenuTitle" = "WireGuard";
|
||||
"macTunnelsMenuTitle" = "Tunnels";
|
||||
"macMenuManageTunnels" = "Manage Tunnels";
|
||||
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
|
||||
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
|
||||
@ -349,6 +358,8 @@
|
||||
|
||||
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
|
||||
|
||||
"macButtonEdit" = "Edit";
|
||||
|
||||
// Mac detail/edit view fields
|
||||
|
||||
"macFieldKey (%@)" = "%@:";
|
||||
@ -413,8 +424,6 @@
|
||||
|
||||
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
|
||||
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
|
||||
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
|
||||
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
|
||||
|
||||
// Mac tooltip
|
||||
|
||||
@ -439,3 +448,7 @@
|
||||
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
|
||||
"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.";
|
||||
|
||||
// Donation
|
||||
|
||||
"donateLink" = "♥ Donate to the WireGuard Project";
|
2
Sources/WireGuardApp/Config/Version.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
VERSION_NAME = 1.0.15
|
||||
VERSION_ID = 26
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
@ -42,50 +42,63 @@ extension ActivateOnDemandOption {
|
||||
}
|
||||
}
|
||||
tunnelProviderManager.onDemandRules = rules
|
||||
tunnelProviderManager.isOnDemandEnabled = self != .off
|
||||
tunnelProviderManager.isOnDemandEnabled = false
|
||||
}
|
||||
|
||||
init(from tunnelProviderManager: NETunnelProviderManager) {
|
||||
let rules = tunnelProviderManager.onDemandRules ?? []
|
||||
let activateOnDemandOption: ActivateOnDemandOption
|
||||
if let onDemandRules = tunnelProviderManager.onDemandRules {
|
||||
self = ActivateOnDemandOption.create(from: onDemandRules)
|
||||
} else {
|
||||
self = .off
|
||||
}
|
||||
}
|
||||
|
||||
private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
|
||||
switch rules.count {
|
||||
case 0:
|
||||
activateOnDemandOption = .off
|
||||
return .off
|
||||
case 1:
|
||||
let rule = rules[0]
|
||||
precondition(rule.action == .connect)
|
||||
activateOnDemandOption = .anyInterface(.anySSID)
|
||||
guard rule.action == .connect else { return .off }
|
||||
return .anyInterface(.anySSID)
|
||||
case 2:
|
||||
let connectRule = rules.first(where: { $0.action == .connect })!
|
||||
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
|
||||
guard let connectRule = rules.first(where: { $0.action == .connect }) else {
|
||||
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 {
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
|
||||
return .wiFiInterfaceOnly(.anySSID)
|
||||
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
|
||||
activateOnDemandOption = .nonWiFiInterfaceOnly
|
||||
return .nonWiFiInterfaceOnly
|
||||
} 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:
|
||||
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
|
||||
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
|
||||
guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
|
||||
guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
|
||||
let ssids = ssidRule.ssidMatch!
|
||||
switch (ssidRule.action, nonWiFiRule.action) {
|
||||
case (.connect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
|
||||
return .anyInterface(.onlySpecificSSIDs(ssids))
|
||||
case (.connect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||
return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
|
||||
case (.disconnect, .connect):
|
||||
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||
return .anyInterface(.exceptSpecificSSIDs(ssids))
|
||||
case (.disconnect, .disconnect):
|
||||
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||
return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
|
||||
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:
|
||||
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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
@ -26,11 +26,11 @@ class MockTunnels {
|
||||
static func createMockTunnels() -> [NETunnelProviderManager] {
|
||||
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.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.allowedIPs = [IPAddressRange(from: allowedIPs)!]
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -67,13 +67,14 @@ extension TunnelConfiguration {
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
throw ParseError.multiplePeersWithSamePublicKey
|
||||
}
|
||||
|
||||
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
|
||||
interfaceConfiguration?.dns = base?.interface.dns ?? []
|
||||
interfaceConfiguration?.dnsSearch = base?.interface.dnsSearch ?? []
|
||||
interfaceConfiguration?.mtu = base?.interface.mtu
|
||||
|
||||
if let interfaceConfiguration = interfaceConfiguration {
|
||||
@ -87,7 +88,7 @@ extension TunnelConfiguration {
|
||||
guard let privateKeyString = attributes["private_key"] else {
|
||||
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)
|
||||
}
|
||||
var interface = InterfaceConfiguration(privateKey: privateKey)
|
||||
@ -106,18 +107,18 @@ extension TunnelConfiguration {
|
||||
guard let publicKeyString = attributes["public_key"] else {
|
||||
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)
|
||||
}
|
||||
var peer = PeerConfiguration(publicKey: publicKey)
|
||||
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)
|
||||
}
|
||||
// TODO(zx2c4): does the compiler optimize this away?
|
||||
var accumulator: UInt8 = 0
|
||||
for index in 0..<preSharedKey.count {
|
||||
accumulator |= preSharedKey[index]
|
||||
for index in 0..<preSharedKey.rawValue.count {
|
||||
accumulator |= preSharedKey.rawValue[index]
|
||||
}
|
||||
if accumulator != 0 {
|
||||
peer.preSharedKey = preSharedKey
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import NetworkExtension
|
||||
|
||||
@ -56,10 +56,10 @@ enum TunnelsManagerActivationError: WireGuardAppError {
|
||||
|
||||
var alertText: AlertText {
|
||||
switch self {
|
||||
case .activationFailed(let wasOnDemandEnabled):
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
||||
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
|
||||
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
|
||||
case .activationFailed:
|
||||
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
|
||||
case .activationFailedWithExtensionError(let title, let message, _):
|
||||
return (title, message)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
@ -1,18 +1,18 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import NetworkExtension
|
||||
import os.log
|
||||
|
||||
protocol TunnelsManagerListDelegate: class {
|
||||
protocol TunnelsManagerListDelegate: AnyObject {
|
||||
func tunnelAdded(at index: Int)
|
||||
func tunnelModified(at index: Int)
|
||||
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
|
||||
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 tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
|
||||
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
|
||||
@ -23,9 +23,9 @@ class TunnelsManager {
|
||||
private var tunnels: [TunnelContainer]
|
||||
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
|
||||
weak var activationDelegate: TunnelsManagerActivationDelegate?
|
||||
private var statusObservationToken: AnyObject?
|
||||
private var waiteeObservationToken: AnyObject?
|
||||
private var configurationsObservationToken: AnyObject?
|
||||
private var statusObservationToken: NotificationToken?
|
||||
private var waiteeObservationToken: NSKeyValueObservation?
|
||||
private var configurationsObservationToken: NotificationToken?
|
||||
|
||||
init(tunnelProviders: [NETunnelProviderManager]) {
|
||||
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
|
||||
@ -58,13 +58,19 @@ class TunnelsManager {
|
||||
#if os(iOS)
|
||||
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
|
||||
#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
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
if let ref = passwordRef {
|
||||
refs.insert(ref)
|
||||
} else {
|
||||
wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
|
||||
tunnelManager.removeFromPreferences { _ in }
|
||||
tunnelManagers.remove(at: index)
|
||||
}
|
||||
@ -132,10 +138,10 @@ class TunnelsManager {
|
||||
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
|
||||
if let error = error {
|
||||
wg_log(.error, message: "Add: Saving configuration failed: \(error)")
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
|
||||
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error)))
|
||||
return
|
||||
}
|
||||
|
||||
@ -163,7 +169,20 @@ class TunnelsManager {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -216,10 +235,10 @@ class TunnelsManager {
|
||||
onDemandOption.apply(on: tunnelProviderManager)
|
||||
|
||||
tunnelProviderManager.saveToPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
|
||||
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
||||
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!
|
||||
wg_log(.error, message: "Modify: Saving configuration failed: \(error)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||
return
|
||||
}
|
||||
guard let self = self else { return }
|
||||
@ -247,12 +266,12 @@ class TunnelsManager {
|
||||
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
|
||||
tunnelProviderManager.loadFromPreferences { error in
|
||||
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
|
||||
return
|
||||
if let error = error {
|
||||
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
}
|
||||
completionHandler(nil)
|
||||
}
|
||||
} else {
|
||||
completionHandler(nil)
|
||||
@ -262,14 +281,19 @@ class TunnelsManager {
|
||||
|
||||
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
|
||||
let tunnelProviderManager = tunnel.tunnelProvider
|
||||
if tunnel.isTunnelConfigurationAvailableInKeychain {
|
||||
#if os(macOS)
|
||||
if tunnel.isTunnelAvailableToUser {
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
}
|
||||
|
||||
#elseif os(iOS)
|
||||
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
|
||||
#else
|
||||
#error("Unimplemented")
|
||||
#endif
|
||||
tunnelProviderManager.removeFromPreferences { [weak self] error in
|
||||
guard error == nil else {
|
||||
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
|
||||
if let error = error {
|
||||
wg_log(.error, message: "Remove: Saving configuration failed: \(error)")
|
||||
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error))
|
||||
return
|
||||
}
|
||||
if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
|
||||
@ -285,7 +309,20 @@ class TunnelsManager {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -305,6 +342,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 {
|
||||
return tunnels.count
|
||||
}
|
||||
@ -352,7 +424,17 @@ class TunnelsManager {
|
||||
tunnel.status = .waiting
|
||||
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
|
||||
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
|
||||
}
|
||||
@ -395,7 +477,7 @@ class TunnelsManager {
|
||||
}
|
||||
|
||||
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,
|
||||
let session = statusChangeNotification.object as? NETunnelProviderSession,
|
||||
let tunnelProvider = session.manager as? NETunnelProviderManager,
|
||||
@ -427,7 +509,7 @@ class TunnelsManager {
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
@ -438,8 +520,8 @@ class TunnelsManager {
|
||||
}
|
||||
}
|
||||
|
||||
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
|
||||
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
||||
static func tunnelNameIsLessThan(_ lhs: String, _ rhs: String) -> Bool {
|
||||
return lhs.compare(rhs, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
@ -461,6 +543,7 @@ class TunnelContainer: NSObject {
|
||||
@objc dynamic var status: TunnelStatus
|
||||
|
||||
@objc dynamic var isActivateOnDemandEnabled: Bool
|
||||
@objc dynamic var hasOnDemandRules: Bool
|
||||
|
||||
var isAttemptingActivation = false {
|
||||
didSet {
|
||||
@ -487,25 +570,33 @@ class TunnelContainer: NSObject {
|
||||
var activationTimer: Timer?
|
||||
var deactivationTimer: Timer?
|
||||
|
||||
fileprivate var tunnelProvider: NETunnelProviderManager
|
||||
fileprivate var tunnelProvider: NETunnelProviderManager {
|
||||
didSet {
|
||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled && tunnelProvider.isEnabled
|
||||
hasOnDemandRules = !(tunnelProvider.onDemandRules ?? []).isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
var tunnelConfiguration: TunnelConfiguration? {
|
||||
return tunnelProvider.tunnelConfiguration
|
||||
}
|
||||
|
||||
var isTunnelConfigurationAvailableInKeychain: Bool {
|
||||
return tunnelProvider.isTunnelConfigurationAvailableInKeychain
|
||||
}
|
||||
|
||||
var onDemandOption: ActivateOnDemandOption {
|
||||
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) {
|
||||
name = tunnel.localizedDescription ?? "Unnamed"
|
||||
let status = TunnelStatus(from: tunnel.connection.status)
|
||||
self.status = status
|
||||
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
|
||||
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled && tunnel.isEnabled
|
||||
hasOnDemandRules = !(tunnel.onDemandRules ?? []).isEmpty
|
||||
tunnelProvider = tunnel
|
||||
super.init()
|
||||
}
|
||||
@ -528,11 +619,10 @@ class TunnelContainer: NSObject {
|
||||
}
|
||||
|
||||
func refreshStatus() {
|
||||
if status == .restarting {
|
||||
if (status == .restarting) || (status == .waiting && tunnelProvider.connection.status == .disconnected) {
|
||||
return
|
||||
}
|
||||
status = TunnelStatus(from: tunnelProvider.connection.status)
|
||||
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
|
||||
}
|
||||
|
||||
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
|
||||
@ -609,18 +699,8 @@ class TunnelContainer: NSObject {
|
||||
}
|
||||
|
||||
extension NETunnelProviderManager {
|
||||
private static var cachedIsConfigAvailableInKeychainKey: 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? {
|
||||
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
|
||||
return cached
|
||||
@ -636,17 +716,9 @@ extension NETunnelProviderManager {
|
||||
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
|
||||
localizedDescription = tunnelConfiguration.name
|
||||
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 {
|
||||
switch (isTunnelConfigurationAvailableInKeychain, tunnel.isTunnelConfigurationAvailableInKeychain) {
|
||||
case (true, true):
|
||||
return tunnelConfiguration == tunnel.tunnelConfiguration
|
||||
case (false, false):
|
||||
return localizedDescription == tunnel.name
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -51,20 +51,18 @@ class ActivateOnDemandViewModel {
|
||||
extension ActivateOnDemandViewModel {
|
||||
convenience init(tunnel: TunnelContainer) {
|
||||
self.init()
|
||||
if tunnel.isActivateOnDemandEnabled {
|
||||
switch tunnel.onDemandOption {
|
||||
case .off:
|
||||
break
|
||||
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
case .nonWiFiInterfaceOnly:
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
case .anyInterface(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
}
|
||||
switch tunnel.onDemandOption {
|
||||
case .off:
|
||||
break
|
||||
case .wiFiInterfaceOnly(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
case .nonWiFiInterfaceOnly:
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
case .anyInterface(let onDemandSSIDOption):
|
||||
isWiFiInterfaceEnabled = true
|
||||
isNonWiFiInterfaceEnabled = true
|
||||
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
protocol ErrorPresenterProtocol {
|
||||
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -44,10 +44,20 @@ class TunnelImporter {
|
||||
}
|
||||
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 {
|
||||
if tunnelConfiguration == nil {
|
||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||
if parseError != nil {
|
||||
if let parseError = parseError as? WireGuardAppError {
|
||||
lastFileImportErrorText = parseError.alertText
|
||||
} else {
|
||||
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
|
||||
}
|
||||
}
|
||||
configs.append(tunnelConfiguration)
|
||||
dispatchGroup.leave()
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -109,9 +109,9 @@ class TunnelViewModel {
|
||||
scratchpad[field] = stringValue
|
||||
}
|
||||
if field == .privateKey {
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength {
|
||||
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? ""
|
||||
scratchpad[.publicKey] = publicKey
|
||||
if stringValue.count == TunnelViewModel.keyLengthInBase64,
|
||||
let privateKey = PrivateKey(base64Key: stringValue) {
|
||||
scratchpad[.publicKey] = privateKey.publicKey.base64Key
|
||||
} else {
|
||||
scratchpad.removeValue(forKey: .publicKey)
|
||||
}
|
||||
@ -128,8 +128,8 @@ class TunnelViewModel {
|
||||
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
|
||||
var scratchpad = [InterfaceField: String]()
|
||||
scratchpad[.name] = name
|
||||
scratchpad[.privateKey] = config.privateKey.base64Key() ?? ""
|
||||
scratchpad[.publicKey] = config.publicKey.base64Key() ?? ""
|
||||
scratchpad[.privateKey] = config.privateKey.base64Key
|
||||
scratchpad[.publicKey] = config.privateKey.publicKey.base64Key
|
||||
if !config.addresses.isEmpty {
|
||||
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
}
|
||||
@ -139,8 +139,10 @@ class TunnelViewModel {
|
||||
if let mtu = config.mtu {
|
||||
scratchpad[.mtu] = String(mtu)
|
||||
}
|
||||
if !config.dns.isEmpty {
|
||||
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
|
||||
if !config.dns.isEmpty || !config.dnsSearch.isEmpty {
|
||||
var dns = config.dns.map { $0.stringRepresentation }
|
||||
dns.append(contentsOf: config.dnsSearch)
|
||||
scratchpad[.dns] = dns.joined(separator: ", ")
|
||||
}
|
||||
return scratchpad
|
||||
}
|
||||
@ -158,7 +160,7 @@ class TunnelViewModel {
|
||||
fieldsWithError.insert(.privateKey)
|
||||
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)
|
||||
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
|
||||
}
|
||||
@ -194,15 +196,16 @@ class TunnelViewModel {
|
||||
}
|
||||
if let dnsString = scratchpad[.dns] {
|
||||
var dnsServers = [DNSServer]()
|
||||
var dnsSearch = [String]()
|
||||
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
|
||||
if let dnsServer = DNSServer(from: dnsServerString) {
|
||||
dnsServers.append(dnsServer)
|
||||
} else {
|
||||
fieldsWithError.insert(.dns)
|
||||
errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
|
||||
dnsSearch.append(dnsServerString)
|
||||
}
|
||||
}
|
||||
config.dns = dnsServers
|
||||
config.dnsSearch = dnsSearch
|
||||
}
|
||||
|
||||
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
|
||||
@ -251,12 +254,12 @@ class TunnelViewModel {
|
||||
var scratchpad = [PeerField: String]()
|
||||
var fieldsWithError = Set<PeerField>()
|
||||
var validatedConfiguration: PeerConfiguration?
|
||||
var publicKey: Data? {
|
||||
var publicKey: PublicKey? {
|
||||
if let validatedConfiguration = validatedConfiguration {
|
||||
return validatedConfiguration.publicKey
|
||||
}
|
||||
if let scratchPadPublicKey = scratchpad[.publicKey] {
|
||||
return Data(base64Key: scratchPadPublicKey)
|
||||
return PublicKey(base64Key: scratchPadPublicKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -301,10 +304,8 @@ class TunnelViewModel {
|
||||
|
||||
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
|
||||
var scratchpad = [PeerField: String]()
|
||||
if let publicKey = config.publicKey.base64Key() {
|
||||
scratchpad[.publicKey] = publicKey
|
||||
}
|
||||
if let preSharedKey = config.preSharedKey?.base64Key() {
|
||||
scratchpad[.publicKey] = config.publicKey.base64Key
|
||||
if let preSharedKey = config.preSharedKey?.base64Key {
|
||||
scratchpad[.preSharedKey] = preSharedKey
|
||||
}
|
||||
if !config.allowedIPs.isEmpty {
|
||||
@ -337,14 +338,14 @@ class TunnelViewModel {
|
||||
fieldsWithError.insert(.publicKey)
|
||||
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)
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
|
||||
}
|
||||
var config = PeerConfiguration(publicKey: publicKey)
|
||||
var errorMessages = [String]()
|
||||
if let preSharedKeyString = scratchpad[.preSharedKey] {
|
||||
if let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
|
||||
if let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) {
|
||||
config.preSharedKey = preSharedKey
|
||||
} else {
|
||||
fieldsWithError.insert(.preSharedKey)
|
||||
@ -397,12 +398,13 @@ class TunnelViewModel {
|
||||
|
||||
static let ipv4DefaultRouteString = "0.0.0.0/0"
|
||||
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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"192.170.0.0/15", "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"
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15",
|
||||
"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) {
|
||||
@ -559,7 +561,7 @@ class TunnelViewModel {
|
||||
}
|
||||
|
||||
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
|
||||
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
|
||||
let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
|
||||
if peerPublicKeysArray.count != peerPublicKeysSet.count {
|
||||
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
@ -21,7 +21,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.backgroundColor = .white
|
||||
self.window = window
|
||||
|
||||
let mainVC = MainViewController()
|
||||
@ -34,10 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
|
||||
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
|
||||
_ = FileManager.deleteFile(at: url)
|
||||
}
|
||||
mainVC?.importFromDisposableFile(url: url)
|
||||
return true
|
||||
}
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 865 B After Width: | Height: | Size: 865 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
@ -64,6 +64,8 @@
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Foundation
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -15,7 +15,7 @@ extension UITableView {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -9,8 +9,8 @@ class ButtonCell: UITableViewCell {
|
||||
set(value) { button.setTitle(value, for: .normal) }
|
||||
}
|
||||
var hasDestructiveAction: Bool {
|
||||
get { return button.tintColor == .red }
|
||||
set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
|
||||
get { return button.tintColor == .systemRed }
|
||||
set(value) { button.tintColor = value ? .systemRed : buttonStandardTintColor }
|
||||
}
|
||||
var onTapped: (() -> Void)?
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -9,6 +9,11 @@ class EditableTextCell: UITableViewCell {
|
||||
set(value) { valueTextField.text = value }
|
||||
}
|
||||
|
||||
var placeholder: String? {
|
||||
get { return valueTextField.placeholder }
|
||||
set(value) { valueTextField.placeholder = value }
|
||||
}
|
||||
|
||||
let valueTextField: UITextField = {
|
||||
let valueTextField = UITextField()
|
||||
valueTextField.textAlignment = .left
|
||||
@ -29,12 +34,13 @@ class EditableTextCell: UITableViewCell {
|
||||
valueTextField.delegate = self
|
||||
contentView.addSubview(valueTextField)
|
||||
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
|
||||
NSLayoutConstraint.activate([
|
||||
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
|
||||
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
|
||||
valueTextField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: valueTextField.trailingAnchor),
|
||||
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: valueTextField.topAnchor),
|
||||
bottomAnchorConstraint
|
||||
])
|
||||
}
|
||||
@ -50,6 +56,7 @@ class EditableTextCell: UITableViewCell {
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
placeholder = nil
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -9,7 +9,11 @@ class KeyValueCell: UITableViewCell {
|
||||
let keyLabel = UILabel()
|
||||
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
keyLabel.adjustsFontForContentSizeCategory = true
|
||||
keyLabel.textColor = .black
|
||||
if #available(iOS 13.0, *) {
|
||||
keyLabel.textColor = .label
|
||||
} else {
|
||||
keyLabel.textColor = .black
|
||||
}
|
||||
keyLabel.textAlignment = .left
|
||||
return keyLabel
|
||||
}()
|
||||
@ -23,7 +27,7 @@ class KeyValueCell: UITableViewCell {
|
||||
}()
|
||||
|
||||
let valueTextField: UITextField = {
|
||||
let valueTextField = UITextField()
|
||||
let valueTextField = KeyValueCellTextField()
|
||||
valueTextField.textAlignment = .right
|
||||
valueTextField.isEnabled = false
|
||||
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
@ -31,7 +35,11 @@ class KeyValueCell: UITableViewCell {
|
||||
valueTextField.autocapitalizationType = .none
|
||||
valueTextField.autocorrectionType = .no
|
||||
valueTextField.spellCheckingType = .no
|
||||
valueTextField.textColor = .gray
|
||||
if #available(iOS 13.0, *) {
|
||||
valueTextField.textColor = .secondaryLabel
|
||||
} else {
|
||||
valueTextField.textColor = .gray
|
||||
}
|
||||
return valueTextField
|
||||
}()
|
||||
|
||||
@ -56,10 +64,18 @@ class KeyValueCell: UITableViewCell {
|
||||
|
||||
var isValueValid = true {
|
||||
didSet {
|
||||
if isValueValid {
|
||||
keyLabel.textColor = .black
|
||||
if #available(iOS 13.0, *) {
|
||||
if isValueValid {
|
||||
keyLabel.textColor = .label
|
||||
} else {
|
||||
keyLabel.textColor = .systemRed
|
||||
}
|
||||
} 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.isActive = true
|
||||
|
||||
contentView.addSubview(valueLabelScrollView)
|
||||
|
||||
contentView.addSubview(valueLabelScrollView)
|
||||
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -16,13 +16,19 @@ class SwitchCell: UITableViewCell {
|
||||
get { return switchView.isEnabled }
|
||||
set(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 observationToken: AnyObject?
|
||||
var statusObservationToken: AnyObject?
|
||||
var isOnDemandEnabledObservationToken: AnyObject?
|
||||
var hasOnDemandRulesObservationToken: AnyObject?
|
||||
|
||||
let switchView = UISwitch()
|
||||
|
||||
@ -47,6 +53,8 @@ class SwitchCell: UITableViewCell {
|
||||
isEnabled = true
|
||||
message = ""
|
||||
isOn = false
|
||||
observationToken = nil
|
||||
statusObservationToken = nil
|
||||
isOnDemandEnabledObservationToken = nil
|
||||
hasOnDemandRulesObservationToken = nil
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -28,7 +28,11 @@ class TextCell: UITableViewCell {
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
message = ""
|
||||
setTextColor(.black)
|
||||
if #available(iOS 13.0, *) {
|
||||
setTextColor(.label)
|
||||
} else {
|
||||
setTextColor(.black)
|
||||
}
|
||||
setTextAlignment(.left)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -30,7 +30,11 @@ class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
copyableGesture = false
|
||||
valueTextField.textColor = .black
|
||||
if #available(iOS 13.0, *) {
|
||||
valueTextField.textColor = .label
|
||||
} else {
|
||||
valueTextField.textColor = .black
|
||||
}
|
||||
valueTextField.isEnabled = true
|
||||
valueLabelScrollView.isScrollEnabled = false
|
||||
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
|
170
Sources/WireGuardApp/UI/iOS/View/TunnelListCell.swift
Normal file
@ -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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -15,9 +15,15 @@ class LogViewController: UIViewController {
|
||||
}()
|
||||
|
||||
let busyIndicator: UIActivityIndicatorView = {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
return busyIndicator
|
||||
if #available(iOS 13.0, *) {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .medium)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
return busyIndicator
|
||||
} else {
|
||||
let busyIndicator = UIActivityIndicatorView(style: .gray)
|
||||
busyIndicator.hidesWhenStopped = true
|
||||
return busyIndicator
|
||||
}
|
||||
}()
|
||||
|
||||
let paragraphStyle: NSParagraphStyle = {
|
||||
@ -35,7 +41,11 @@ class LogViewController: UIViewController {
|
||||
|
||||
override func loadView() {
|
||||
view = UIView()
|
||||
view.backgroundColor = .white
|
||||
if #available(iOS 13.0, *) {
|
||||
view.backgroundColor = .systemBackground
|
||||
} else {
|
||||
view.backgroundColor = .white
|
||||
}
|
||||
|
||||
view.addSubview(textView)
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -81,12 +91,18 @@ class LogViewController: UIViewController {
|
||||
let richText = NSMutableAttributedString()
|
||||
let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
|
||||
let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
|
||||
let lightGrayColor = UIColor(white: 0.88, alpha: 1.0)
|
||||
|
||||
for logEntry in fetchedLogEntries {
|
||||
let bgColor = self.isNextLineHighlighted ? lightGrayColor : UIColor.white
|
||||
let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
|
||||
let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
|
||||
var bgColor: UIColor
|
||||
var fgColor: UIColor
|
||||
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(messageText)
|
||||
self.isNextLineHighlighted.toggle()
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -11,7 +11,11 @@ class MainViewController: UISplitViewController {
|
||||
|
||||
init() {
|
||||
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 masterVC = TunnelsListTableViewController()
|
||||
@ -109,6 +113,19 @@ extension MainViewController {
|
||||
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 {
|
@ -1,10 +1,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import AVFoundation
|
||||
import UIKit
|
||||
|
||||
protocol QRScanViewControllerDelegate: class {
|
||||
protocol QRScanViewControllerDelegate: AnyObject {
|
||||
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
@ -1,10 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import SystemConfiguration.CaptiveNetwork
|
||||
import NetworkExtension
|
||||
|
||||
protocol SSIDOptionEditTableViewControllerDelegate: class {
|
||||
protocol SSIDOptionEditTableViewControllerDelegate: AnyObject {
|
||||
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
|
||||
}
|
||||
|
||||
@ -39,9 +40,16 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
||||
selectedOption = option
|
||||
selectedSSIDs = ssids
|
||||
super.init(style: .grouped)
|
||||
connectedSSID = getConnectedSSID()
|
||||
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) {
|
||||
@ -60,6 +68,7 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
||||
tableView.register(TextCell.self)
|
||||
tableView.isEditing = true
|
||||
tableView.allowsSelectionDuringEditing = true
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
}
|
||||
|
||||
func loadSections() {
|
||||
@ -71,14 +80,14 @@ class SSIDOptionEditTableViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func loadAddSSIDRows() {
|
||||
addSSIDRows.removeAll()
|
||||
if let connectedSSID = connectedSSID {
|
||||
if !selectedSSIDs.contains(connectedSSID) {
|
||||
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
|
||||
func updateCurrentSSIDEntry() {
|
||||
if let connectedSSID = connectedSSID, !selectedSSIDs.contains(connectedSSID) {
|
||||
if let first = addSSIDRows.first, case .addNewSSID = first {
|
||||
addSSIDRows.insert(.addConnectedSSID(connectedSSID: connectedSSID), at: 0)
|
||||
}
|
||||
} else if let first = addSSIDRows.first, case .addConnectedSSID = first {
|
||||
addSSIDRows.removeFirst()
|
||||
}
|
||||
addSSIDRows.append(.addNewSSID)
|
||||
}
|
||||
|
||||
func updateTableViewAddSSIDRows() {
|
||||
@ -176,7 +185,11 @@ extension SSIDOptionEditTableViewController {
|
||||
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = tr("tunnelOnDemandNoSSIDs")
|
||||
cell.setTextColor(.gray)
|
||||
if #available(iOS 13.0, *) {
|
||||
cell.setTextColor(.secondaryLabel)
|
||||
} else {
|
||||
cell.setTextColor(.gray)
|
||||
}
|
||||
cell.setTextAlignment(.center)
|
||||
return cell
|
||||
}
|
||||
@ -184,12 +197,13 @@ extension SSIDOptionEditTableViewController {
|
||||
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.message = selectedSSIDs[indexPath.row]
|
||||
cell.placeholder = tr("tunnelOnDemandSSIDTextFieldPlaceholder")
|
||||
cell.isEditing = true
|
||||
cell.onValueBeingEdited = { [weak self, weak cell] text in
|
||||
guard let self = self, let cell = cell else { return }
|
||||
if let row = self.tableView.indexPath(for: cell)?.row {
|
||||
self.selectedSSIDs[row] = text
|
||||
self.loadAddSSIDRows()
|
||||
self.updateCurrentSSIDEntry()
|
||||
self.updateTableViewAddSSIDRows()
|
||||
}
|
||||
}
|
||||
@ -220,7 +234,7 @@ extension SSIDOptionEditTableViewController {
|
||||
} else {
|
||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateCurrentSSIDEntry()
|
||||
updateTableViewAddSSIDRows()
|
||||
case .addSSIDs:
|
||||
assert(editingStyle == .insert)
|
||||
@ -240,7 +254,7 @@ extension SSIDOptionEditTableViewController {
|
||||
} else {
|
||||
tableView.insertRows(at: [indexPath], with: .automatic)
|
||||
}
|
||||
loadAddSSIDRows()
|
||||
updateCurrentSSIDEntry()
|
||||
updateTableViewAddSSIDRows()
|
||||
if newSSID.isEmpty {
|
||||
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 {
|
||||
@ -266,6 +305,10 @@ extension SSIDOptionEditTableViewController {
|
||||
case .ssidOption:
|
||||
let previousOption = selectedOption
|
||||
selectedOption = ssidOptionFields[indexPath.row]
|
||||
guard previousOption != selectedOption else {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
return
|
||||
}
|
||||
loadSections()
|
||||
if previousOption == .anySSID {
|
||||
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
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import os.log
|
||||
@ -144,8 +144,8 @@ extension SettingsTableViewController {
|
||||
cell.copyableGesture = false
|
||||
cell.key = field.localizedUIString
|
||||
if field == .iosAppVersion {
|
||||
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
var appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown version"
|
||||
if let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
|
||||
appVersion += " (\(appBuild))"
|
||||
}
|
||||
cell.value = appVersion
|
||||
@ -160,8 +160,7 @@ extension SettingsTableViewController {
|
||||
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
|
||||
}
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .viewLog)
|
||||
} else if field == .viewLog {
|
||||
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.buttonText = field.localizedUIString
|
||||
cell.onTapped = { [weak self] in
|
||||
@ -169,5 +168,6 @@ extension SettingsTableViewController {
|
||||
}
|
||||
return cell
|
||||
}
|
||||
fatalError()
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@ -120,7 +120,7 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
|
||||
editVC.delegate = self
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
editNC.modalPresentationStyle = .fullScreen
|
||||
self.present(editNC, animated: true)
|
||||
}
|
||||
}
|
||||
@ -152,8 +152,8 @@ class TunnelDetailTableViewController: UITableViewController {
|
||||
}
|
||||
}!
|
||||
let firstPeerSectionIndex = interfaceSectionIndex + 1
|
||||
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
||||
var peerFieldIsVisible = self.peerFieldIsVisible
|
||||
let interfaceFieldIsVisible = self.interfaceFieldIsVisible
|
||||
let peerFieldIsVisible = self.peerFieldIsVisible
|
||||
|
||||
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
|
||||
for (index, field) in fields.enumerated() {
|
||||
@ -324,8 +324,22 @@ extension TunnelDetailTableViewController {
|
||||
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
|
||||
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
|
||||
let text: String
|
||||
func update(cell: SwitchCell?, with tunnel: TunnelContainer) {
|
||||
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 {
|
||||
case .inactive:
|
||||
text = tr("tunnelStatusInactive")
|
||||
@ -342,26 +356,49 @@ extension TunnelDetailTableViewController {
|
||||
case .waiting:
|
||||
text = tr("tunnelStatusWaiting")
|
||||
}
|
||||
cell.textLabel?.text = text
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
|
||||
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
|
||||
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
|
||||
|
||||
if tunnel.hasOnDemandRules {
|
||||
text += isOnDemandEngaged ? tr("tunnelStatusAddendumOnDemand") : ""
|
||||
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)
|
||||
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||
guard let cell = cell else { return }
|
||||
statusUpdate(cell, tunnel.status)
|
||||
update(cell: cell, with: tunnel)
|
||||
cell.statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
|
||||
update(cell: cell, with: tunnel)
|
||||
}
|
||||
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
|
||||
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 {
|
||||
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
||||
if isOn {
|
||||
self.tunnelsManager.startActivation(of: self.tunnel)
|
||||
} else {
|
||||
self.tunnelsManager.startDeactivation(of: self.tunnel)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cell
|
||||
@ -397,6 +434,7 @@ extension TunnelDetailTableViewController {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = onDemandViewModel.localizedInterfaceDescription
|
||||
cell.copyableGesture = false
|
||||
return cell
|
||||
} else {
|
||||
assert(field == .ssid)
|
||||
@ -404,6 +442,7 @@ extension TunnelDetailTableViewController {
|
||||
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
|
||||
cell.key = field.localizedUIString
|
||||
cell.value = onDemandViewModel.ssidOption.localizedUIString
|
||||
cell.copyableGesture = false
|
||||
return cell
|
||||
} else {
|
||||
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
|
@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol TunnelEditTableViewControllerDelegate: class {
|
||||
protocol TunnelEditTableViewControllerDelegate: AnyObject {
|
||||
func tunnelSaved(tunnel: TunnelContainer)
|
||||
func tunnelEditingCancelled()
|
||||
}
|
||||
@ -214,7 +214,7 @@ extension TunnelEditTableViewController {
|
||||
cell.onTapped = { [weak self] in
|
||||
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),
|
||||
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
|
||||
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
|
||||
@ -266,7 +266,7 @@ extension TunnelEditTableViewController {
|
||||
guard let self = self else { return }
|
||||
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
|
||||
if isAllowedIPsChanged {
|
||||
let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } }
|
||||
let section = self.sections.firstIndex { if case .peer = $0 { return true } else { return false } }
|
||||
if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
|
||||
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
|
||||
}
|
||||
@ -318,7 +318,7 @@ extension TunnelEditTableViewController {
|
||||
let removedSectionIndices = self.deletePeer(peer: peerData)
|
||||
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
|
||||
|
||||
//swiftlint:disable:next trailing_closure
|
||||
// swiftlint:disable:next trailing_closure
|
||||
tableView.performBatchUpdates({
|
||||
self.tableView.deleteSections(removedSectionIndices, with: .fade)
|
||||
if shouldShowExcludePrivateIPs {
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
@ -32,7 +32,12 @@ class TunnelsListTableViewController: UIViewController {
|
||||
}()
|
||||
|
||||
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
|
||||
return busyIndicator
|
||||
}()
|
||||
@ -46,7 +51,11 @@ class TunnelsListTableViewController: UIViewController {
|
||||
|
||||
override func loadView() {
|
||||
view = UIView()
|
||||
view.backgroundColor = .white
|
||||
if #available(iOS 13.0, *) {
|
||||
view.backgroundColor = .systemBackground
|
||||
} else {
|
||||
view.backgroundColor = .white
|
||||
}
|
||||
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
@ -178,7 +187,7 @@ class TunnelsListTableViewController: UIViewController {
|
||||
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
|
||||
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
|
||||
let editNC = UINavigationController(rootViewController: editVC)
|
||||
editNC.modalPresentationStyle = .formSheet
|
||||
editNC.modalPresentationStyle = .fullScreen
|
||||
present(editNC, animated: true)
|
||||
}
|
||||
|
||||
@ -308,10 +317,18 @@ extension TunnelsListTableViewController: UITableViewDataSource {
|
||||
cell.tunnel = tunnel
|
||||
cell.onSwitchToggled = { [weak self] isOn in
|
||||
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
|
||||
if isOn {
|
||||
tunnelsManager.startActivation(of: tunnel)
|
||||
if tunnel.hasOnDemandRules {
|
||||
tunnelsManager.setOnDemandEnabled(isOn, on: tunnel) { error in
|
||||
if error == nil && !isOn {
|
||||
tunnelsManager.startDeactivation(of: tunnel)
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
} else {
|
||||
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)
|
||||
splitViewController.showDetailViewController(detailNC, sender: self)
|
||||
}
|
||||
@ -402,12 +423,12 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
|
||||
}
|
||||
|
||||
extension UISplitViewController {
|
||||
func showDetailViewController(_ vc: UIViewController, sender: Any?, animated: Bool) {
|
||||
func showDetailViewController(_ viewController: UIViewController, sender: Any?, animated: Bool) {
|
||||
if animated {
|
||||
showDetailViewController(vc, sender: sender)
|
||||
showDetailViewController(viewController, sender: sender)
|
||||
} else {
|
||||
UIView.performWithoutAnimation {
|
||||
showDetailViewController(vc, sender: sender)
|
||||
showDetailViewController(viewController, sender: sender)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
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() {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = tr("macConfirmAndQuitAlertMessage")
|
||||
@ -213,7 +200,8 @@ extension AppDelegate {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
NSApp.orderFrontStandardAboutPanel(options: [
|
||||
.applicationVersion: appVersionString,
|
||||
.version: ""
|
||||
.version: "",
|
||||
.credits: ""
|
||||
])
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
|
||||
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Application: NSApplication {
|
||||
|
||||
private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate
|
||||
private var appDelegate: AppDelegate? // swiftlint:disable:this weak_delegate
|
||||
|
||||
override 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 |