Compare commits

...

70 Commits

Author SHA1 Message Date
f3798d0e11 App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 20:59:19 +02:00
86afd1a46a UI: iOS: disable list rows when no config
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 20:56:25 +02:00
7171df84fa WireGuardApp: use file to communicate launch-by-login-helper
Apple event params are broken on recent macOS versions.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 19:22:44 +02:00
d882a486a9 Keychain: remove class constraint when copying
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 16:51:25 +02:00
adcbd17ebe WireGuardApp: do not delete unverifying profiles ever
The Keychain code is much too fragile, and it's better to err on the
safe side. Instead just log an error when this happens.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 07:13:48 +02:00
3d8de22b96 WireGuardKitGo: bump wireguard-go version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 06:58:14 +02:00
ba4d1e7b21 MacAppStoreUpdateDetector: Detect StoreAEService correctly
In macOS 10.15 and macOS 11, the quit Apple event is sent by:
  com.apple.AppStoreDaemon.StoreAEService

In some earlier macOS release, the quit Apple event was sent by:
  com.apple.CommerceKit.StoreAEService

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
f5a14b8434 MacAppStoreUpdateDetector: Add pid to the log
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
b74eb7239a WireGuardKitGo: include new homebrew location in PATH
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 06:58:14 +02:00
a8226b35d2 build: Fix swiftlint warnings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
73c708d902 build: Fix swift warnings
Use 'AnyObject' instead of 'class' to restrict protocol inheritance

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
3668f3af9f build: Include 'swiftlint' location in the PATH before invoking it
In macOS 11, HomeBrew installs swiftlint under /opt/homebrew, which is not
in the default path that Xcode seems to use. So we include the PATH
to contain:

  - /usr/local/bin:

    Where HomeBrew installs 'swiftlint' in macOS 10.15 and earlier

  - /opt/homebrew/bin:

    Where HomeBrew installs 'swiftlint' in macOS 11

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
54697a3240 UI: Use 'On-Demand', with hyphen, consistently
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 06:58:14 +02:00
3428bfbc9e UI: macOS: do on-demand ritual for clicking list item too
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 02:40:49 +02:00
cfd1b16801 UI: Consider on-demand to be enabled iff the tunnel provider is enabled
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-08-03 16:35:18 +05:30
ca70fe9ddc UI: When setting on-demand, avoid a second saveToPreferences() call
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-08-03 16:34:57 +05:30
55c587b443 UI: When saving on-demand rules, don't set isOnDemandEnabled
When adding or modifying a config, when on-demand options are set by a
user, the rules are saved, but isOnDemandEnabled is left unset (and can
be set by the appropriate control in the detail view (switch in iOS /
button in macOS)).

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-08-02 23:25:53 +05:30
b6831c1aca UI: macOS: Incorporate on-demand-ness in status menu
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:38 +05:30
2ac17da7cb UI: macOS: Tunnel detail: Incorporate on-demand-ness in toggle button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:33 +05:30
274c4cd092 UI: macOS: Tunnel detail: Incorporate on-demand-ness in the status row
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:28 +05:30
95e1409bfb UI: macOS: Tunnel list: Incorporate on-demand-ness in the status circle
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:25 +05:30
2c2c53b1f8 UI: macOS: Add yellow circle image
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:09 +05:30
9cbfec99df UI: Localizations: Remove alertTunnelActivationFailureOnDemandAddendum
It's not used anymore.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 15:55:17 +05:30
1bd6dcb7e7 UI: Remove addendum on on-demand from error on tunnel activation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 11:52:54 +05:30
c1fe8b0162 UI: When setting on-demand, enable the tunnel if required
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 11:52:54 +05:30
64c2fb337d UI: iOS: Tunnels list: Move the "On Demand" label to the right
Having that at the bottom makes it harder for iOS to get
the row height correctly.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:35:05 +05:30
147ac02f0d UI: iOS: Show on-demand state in 'Status' if there are on-demand rules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:35:05 +05:30
03ef79c0fd UI: When reloading tunnels, preserve '.waiting' state
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:35:05 +05:30
a261d84fc6 UI: When deactivating for activating another tunnel, disable on-demand
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:35:05 +05:30
abaf1f1454 UI: Keep on-demand rules even if on-demand is disabled
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:34:36 +05:30
1e9e21bacf UI: iOS: Tunnel detail: Incorporate on-demand-ness in 'Status'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:02 +05:30
ac9f7b9f5e UI: iOS: Show "on-demand is active" for tunnels with the active on-demand
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:02 +05:30
a115dd3bd9 UI: iOS: Tunnels list: Incorporate on-demand-ness in the switch
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:01 +05:30
df9934a4b8 UI: TunnelsManager: Add setOnDemandEnabled() instance method
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:01 +05:30
40f18de4d2 UI: TunnelsManager: Add TunnelContainer.hasOnDemandRules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:01 +05:30
13b720442d Global: bump copyright year
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-17 16:56:46 +02:00
c1f509d65b Kit: add missing import for WireGuardKitC
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2021-06-17 15:15:41 +02:00
87f0526f09 App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 18:34:54 +02:00
060c027325 Kit: Go: mod bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 18:03:28 +02:00
23bf3cfccb Kit: Adapter: use more reliable utun detection technique
Rather than hoping that the AF_SYSTEM fd is of type utun, and then
calling "2" on it to get the name -- which could be defined as something
else for a different AF_SYSTEM socket type -- instead simply query the
AF_SYSTEM control socket ID with getpeername. This has one catch, which
is that the ID is dynamically allocated, so we resolve it using the
qualified name. Normally we'd make a new AF_SYSTEM socket for this, but
since that's not allowed in the sandbox, we reuse the AF_SYSTEM socket
that we're checking. At this point in the flow, we know that it's a
proper AF_SYSTEM one, based on the first sockaddr member; we just don't
know that it's a utun variety.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 17:40:12 +02:00
7f5ad3e503 Kit: Adapter: iterate through all FDs to find UTUN
This is a bit of a kludge, until I find something better. We simply
iterate through all FDs, and call getsockopt on each one until we find
the utun FD. This works, and completes rather quickly (fd is usually 6
or 7). Rather than maintain the old path for older kernels, just use
this for all versions, to get more coverage. Other techniques involve
undocumented APIs; this one has the advantage of using nothing
undocumented.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 15:56:21 +02:00
820fa55380 SPM: update exclude rules
Fixes missing excluded file warning in Xcode. api-ios.go was renamed to api-apple.go.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2021-06-16 15:23:11 +02:00
eb528c766b UI: iOS: asynchronously load from NEHotspotNetwork on iOS 14
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:07 -07:00
53235eb38f UI: iOS: clean up visuals in SSID editor
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:07 -07:00
b9ff5c2e94 README: account for funky xcode paths
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:07 -07:00
b7f69d20b6 Kit: Go: bump to latest API
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:04 -07:00
6c4f4109eb UI: iOS: Disable "copy" action on on-demand cells
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2021-01-11 13:09:41 +01:00
7b5b564a6e Kit: netcfg: add explicit IP mask routes
macOS will use the wrong source address unless we add explicit routes
that mention the self-pointing gateway. Actually, it won't add any
implicit routes on its own, so in order to route the masks of the
addresses, we have to add our own routes explicitly.

However, this still doesn't fix the problem while inside of the network
extension, even though it works outside it.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-01-01 18:28:14 +01:00
695f868b1f Kit: Go: mod bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 22:54:47 +01:00
e724c043d9 UI: iOS: Remove duplicate call to addSubview
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-23 16:14:03 +01:00
491301f58b UI: iOS: Fix placeholder label alignment in text fields.
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-23 16:14:03 +01:00
c4f79beb8d App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 15:05:56 +01:00
a613fec2ff project: sync translations and improve id generation again
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 14:55:29 +01:00
e54a5d9a13 UI: macOS: Group more than 10 tunnels into submenu
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-23 14:40:54 +01:00
6d57c8b6f9 UI: Avoid force unwrap when checking for errors
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-23 14:17:59 +01:00
b67acaccff Kit: do not crash on [abcd::] with missing port
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 14:06:26 +01:00
d8568b0e31 Kit: Go: bump module and simplify API
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 13:40:24 +01:00
373bb2ae99 UI: pause VPN configurations observer while adding or removing multiple tunnels
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-22 12:47:02 +01:00
631286e2d1 UI: use NotificationToken to properly clean up observers
When the variable goes out of scope, the observer isn't removed unless
an explicit call is made to the token.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-22 12:46:30 +01:00
74cd7041dc Keychain: prevent call to stat() when determining appex path
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-22 12:46:14 +01:00
21d920c8b0 Kit: Go: use Windows-style retry sleep loop on bind updates
Something odd happens in the network extension that we still don't
understand. Attempt to poke it in this terrible way.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-18 23:53:39 +01:00
44c4df1cd5 UI: Model: remove 0.0.0.0/8 from non-private IPs
macOS freaks out if you try to explicitly route to 0.0.0.0/8 in its
includedRoutes parameter. Even though 0.0.0.0/8 isn't RFC1918, it is
marked in RFC6890 as "this host on this network", so removing it from
the Internet routes makes sense semantically too.

This commit changes 0.0.0.0/5 into:
- 1.0.0.0/8
- 2.0.0.0/8
- 3.0.0.0/8
- 4.0.0.0/6

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-18 23:53:39 +01:00
a4fc0f64b8 UI: iOS: remove donation link
Apple forbids us from having a simple link to wireguard.com/donations/
in the version info window, citing the existence of this link as a form
of payment outside of their in-app purchase framework that requires 30%.
The link had been there for around two years. After rejecting an app
update for a critical networking regression unrelated to this, they
wrote:

    Dec 17, 2020 at 8:35 PM
    From Apple

    3.1.1 - Business - Payments - In-App Purchase

    We noticed that your app allows users to contribute donations to the
    development of your app with a mechanism other than the in-app
    purchase API, which is not appropriate for the App Store.

    Next Steps

    To resolve this issue, please revise your app to use the in-app
    purchase API to pay for this type of transaction. Please note that
    even though tipping another individual is optional, the tip is
    connected to or associated with the receipt of digital content or
    services in your app and must be purchased through in-app purchase
    in accordance with guideline 3.1.1 of the App Store Review
    Guidelines.

    Please see attached screenshot for details.

Trying to appeal this or reason with Apple is not going to be a fruitful
endeavor, so instead we simply cut our losses and remove the donation
link entirely. The goal, anyway, is to get a timely critical update into
the hands of users, and encouraging Apple to block that further would be
a disservice.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 22:44:30 +01:00
9269c7c1c1 UI: macOS: Fix UTF-8 and UTF-16 conversions in highlighter code
NSString uses UTF-16 internally, while String uses UTF-8 in Swift 5.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-17 17:36:46 +01:00
403ee63615 project: generate more stable locale IDs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 12:41:23 +01:00
b622fde291 build: disable hardened runtime on iOS but keep it enabled on macOS
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-17 11:58:50 +01:00
386fe4eb12 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 00:43:38 +01:00
49b7d083f1 UI: add missing translations to incomplete locales
This is the wrong way to fix the problem. The correct way will involve
moving away from the whacky tr() macro and using translations functions
properly. But migrating to that will require some heavy scripting work.
So for now, use a hammer.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 00:40:42 +01:00
db4e2915f3 Kit: Adapter: do not treat NE settings timeouts as fatal
The general Network Extension framework is incredibly buggy, and a
timeout when setting the network settings does not necessarily imply
that the whole operation failed. Simply log the condition and move on.
This restores the app's old behavior.

Reported-by: Filipe Mendonça <cfilipem@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 00:26:00 +01:00
20bdf46792 Kit: PacketTunnelSettingsGenerator: do not require DNS queries if no DNS
Prior, we would set matchDomains=[""] even if the user didn't provide
any DNS servers. This was kind of incoherent, but I guess we had in mind
some kind of non-sensical leakproof scheme that never really worked
anyway. NetworkExtension didn't like this, so setTunnelNetworkSettings
would, rather than return an error, simply timeout and never call its
callback function. But everything worked fine, so we had code in the UI
to check to make sure everything was okay after 5 seconds or so of no
callback. Recent changes made the timeout fatal on the network extension
side, so rather than succeed, configs with no DNS server started
erroring out, causing user reports.

This commit attempts to handle the root cause of the timeout issue by
not twiddling with DNS settings if no DNS server was specified. For now,
however, it leaves the hard-timeout semantics in place.

Reported-by: Filipe Mendonça <cfilipem@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 00:10:28 +01:00
136 changed files with 2882 additions and 629 deletions

View File

@ -1,4 +1,4 @@
Copyright © 2018-2020 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

View File

@ -30,7 +30,7 @@ let package = Package(
"goruntime-boottime-over-monotonic.diff",
"go.mod",
"go.sum",
"api-ios.go",
"api-apple.go",
"Makefile"
],
publicHeadersPath: ".",

View File

@ -54,7 +54,7 @@ $ open WireGuard.xcodeproj
the "External Build Tool Configuration":
```
$BUILD_DIR/../../SourcePackages/checkouts/wireguard-apple/Sources/WireGuardKitGo
${BUILD_DIR%Build/*}SourcePackages/checkouts/wireguard-apple/Sources/WireGuardKitGo
```
- Switch to "Build Settings" and find `SDKROOT`.

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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)

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
import Security
@ -7,8 +7,7 @@ import Security
class Keychain {
static func openReference(called ref: Data) -> String? {
var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecClass: kSecClassGenericPassword,
kSecValuePersistentRef: ref,
let ret = SecItemCopyMatching([kSecValuePersistentRef: ref,
kSecReturnData: true] as CFDictionary,
&result)
if ret != errSecSuccess || result == nil {
@ -44,7 +43,7 @@ class Keychain {
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
}
@ -109,8 +108,7 @@ class Keychain {
}
static func verifyReference(called ref: Data) -> Bool {
return SecItemCopyMatching([kSecClass: kSecClassGenericPassword,
kSecValuePersistentRef: ref] as CFDictionary,
return SecItemCopyMatching([kSecValuePersistentRef: ref] as CFDictionary,
nil) != errSecItemNotFound
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
import os.log

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
*/
#include <string.h>

View File

@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
* Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
*/
#ifndef RINGLOGGER_H

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View 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)
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
// iOS permission prompts

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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…";

View File

@ -1,2 +1,2 @@
VERSION_NAME = 1.0.10
VERSION_ID = 19
VERSION_NAME = 1.0.14
VERSION_ID = 25

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension
@ -42,11 +42,11 @@ extension ActivateOnDemandOption {
}
}
tunnelProviderManager.onDemandRules = rules
tunnelProviderManager.isOnDemandEnabled = self != .off
tunnelProviderManager.isOnDemandEnabled = false
}
init(from tunnelProviderManager: NETunnelProviderManager) {
if tunnelProviderManager.isOnDemandEnabled, let onDemandRules = tunnelProviderManager.onDemandRules {
if let onDemandRules = tunnelProviderManager.onDemandRules {
self = ActivateOnDemandOption.create(from: onDemandRules)
} else {
self = .off

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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)
}
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension

View File

@ -1,18 +1,18 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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) }
@ -56,21 +56,19 @@ class TunnelsManager {
tunnelManager.saveToPreferences { _ in }
}
#if os(iOS)
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
let verify = true
#elseif os(macOS)
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
}
let verify = proto.providerConfiguration?["UID"] as? uid_t == getuid()
#else
#error("Unimplemented")
#endif
if let ref = passwordRef {
if verify && !proto.verifyConfigurationReference() {
wg_log(.error, message: "Unable to verify keychain entry of tunnel: \(tunnelManager.localizedDescription ?? "<unknown>")")
}
if let ref = proto.passwordReference {
refs.insert(ref)
} else {
wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
wg_log(.error, message: "Removing orphaned tunnel with missing keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
tunnelManager.removeFromPreferences { _ in }
tunnelManagers.remove(at: index)
}
@ -138,10 +136,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
}
@ -169,7 +167,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) {
@ -222,10 +233,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 }
@ -253,12 +264,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)
@ -278,9 +289,9 @@ class TunnelsManager {
#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) {
@ -296,7 +307,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) {
@ -316,6 +340,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
}
@ -363,7 +422,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
}
@ -406,7 +475,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,
@ -438,7 +507,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
@ -472,6 +541,7 @@ class TunnelContainer: NSObject {
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool
@objc dynamic var hasOnDemandRules: Bool
var isAttemptingActivation = false {
didSet {
@ -498,7 +568,12 @@ 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
@ -518,7 +593,8 @@ class TunnelContainer: NSObject {
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()
}
@ -541,11 +617,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?) {

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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)
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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)?)

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
import LocalAuthentication

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
@ -398,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) {

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
@ -27,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)
@ -115,8 +115,6 @@ class KeyValueCell: UITableViewCell {
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
contentView.addSubview(valueLabelScrollView)
contentView.addSubview(valueLabelScrollView)
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
@ -234,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)
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
@ -26,7 +26,9 @@ class SwitchCell: UITableViewCell {
var onSwitchToggled: ((Bool) -> Void)?
var observationToken: AnyObject?
var statusObservationToken: AnyObject?
var isOnDemandEnabledObservationToken: AnyObject?
var hasOnDemandRulesObservationToken: AnyObject?
let switchView = UISwitch()
@ -51,6 +53,8 @@ class SwitchCell: UITableViewCell {
isEnabled = true
message = ""
isOn = false
observationToken = nil
statusObservationToken = nil
isOnDemandEnabledObservationToken = nil
hasOnDemandRulesObservationToken = nil
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
@ -12,9 +12,16 @@ class TunnelListCell: UITableViewCell {
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status, animated: false)
update(from: tunnel, animated: false)
statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status, animated: true)
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)
}
}
}
@ -28,6 +35,20 @@ class TunnelListCell: UITableViewCell {
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, *) {
@ -41,20 +62,26 @@ class TunnelListCell: UITableViewCell {
let statusSwitch = UISwitch()
private var statusObservationToken: NSKeyValueObservation?
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, nameLabel] {
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)
@ -64,13 +91,18 @@ class TunnelListCell: UITableViewCell {
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),
nameLabelBottomConstraint,
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(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1)
busyIndicator.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1)
])
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
@ -94,21 +126,47 @@ class TunnelListCell: UITableViewCell {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?, animated: Bool) {
guard let status = status else {
private func update(from tunnel: TunnelContainer?, animated: Bool) {
guard let tunnel = tunnel else {
reset(animated: animated)
return
}
statusSwitch.setOn(!(status == .deactivating || status == .inactive), animated: animated)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
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 {
busyIndicator.startAnimating()
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)
}
if tunnel.tunnelConfiguration == nil {
statusSwitch.isUserInteractionEnabled = false
backgroundColor = .systemPink
}
}
private func reset(animated: Bool) {
statusSwitch.thumbTintColor = nil
statusSwitch.setOn(false, animated: animated)
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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)?)
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit

View File

@ -1,10 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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() {
@ -188,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()
}
}
@ -224,7 +234,7 @@ extension SSIDOptionEditTableViewController {
} else {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
loadAddSSIDRows()
updateCurrentSSIDEntry()
updateTableViewAddSSIDRows()
case .addSSIDs:
assert(editingStyle == .insert)
@ -244,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 {
@ -253,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 {
@ -289,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
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
@ -11,7 +11,6 @@ class SettingsTableViewController: UITableViewController {
case goBackendVersion
case exportZipArchive
case viewLog
case donateLink
var localizedUIString: String {
switch self {
@ -19,13 +18,12 @@ class SettingsTableViewController: UITableViewController {
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
case .exportZipArchive: return tr("settingsExportZipButtonTitle")
case .viewLog: return tr("settingsViewLogButtonTitle")
case .donateLink: return tr("donateLink")
}
}
}
let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion, .donateLink],
[.iosAppVersion, .goBackendVersion],
[.exportZipArchive],
[.viewLog]
]
@ -169,15 +167,6 @@ extension SettingsTableViewController {
self?.presentLogView()
}
return cell
} else if field == .donateLink {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.localizedUIString
cell.onTapped = {
if let url = URL(string: "https://www.wireguard.com/donations/"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
}
return cell
}
fatalError()
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
@ -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,24 +356,49 @@ extension TunnelDetailTableViewController {
case .waiting:
text = tr("tunnelStatusWaiting")
}
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)
}
if tunnel.hasOnDemandRules && !isOnDemandEngaged && status == .inactive {
text = tr("tunnelStatusOnDemandDisabled")
}
cell.textLabel?.text = text
cell.switchView.isOn = !(status == .deactivating || status == .inactive)
cell.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
cell.isEnabled = status == .active || status == .inactive
}
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
@ -395,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)
@ -402,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)

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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()
}
@ -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 {

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
@ -317,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)
}
}
}
}
@ -336,6 +344,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
}
guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
guard tunnel.tunnelConfiguration != nil else { return }
showTunnelDetail(for: tunnel, animated: true)
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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")

View File

@ -1,11 +1,11 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 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()

View File

@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "StatusCircleYellow@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "StatusCircleYellow@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -29,7 +29,7 @@
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.</string>
<string>Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.</string>
<key>NSPrincipalClass</key>
<string>WireGuard.Application</string>
<key>LSApplicationCategoryType</key>

View File

@ -1,28 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
class LaunchedAtLoginDetector {
static let launchCode = "LaunchedByWireGuardLoginItemHelper"
static func isLaunchedAtLogin(openAppleEvent: NSAppleEventDescriptor) -> Bool {
guard isOpenEvent(openAppleEvent) else { return false }
guard let propData = openAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
return propData.stringValue == launchCode
}
static func isReopenedByLoginItemHelper(reopenAppleEvent: NSAppleEventDescriptor) -> Bool {
guard isReopenEvent(reopenAppleEvent) else { return false }
guard let propData = reopenAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
return propData.stringValue == launchCode
let now = clock_gettime_nsec_np(CLOCK_UPTIME_RAW)
guard openAppleEvent.eventClass == kCoreEventClass && openAppleEvent.eventID == kAEOpenApplication else { return false }
guard let url = FileManager.loginHelperTimestampURL else { return false }
guard let data = try? Data(contentsOf: url) else { return false }
_ = FileManager.deleteFile(at: url)
guard data.count == 8 else { return false }
let then = data.withUnsafeBytes { ptr in
ptr.load(as: UInt64.self)
}
return now - then <= 5000000000
}
}
private func isOpenEvent(_ event: NSAppleEventDescriptor) -> Bool {
return event.eventClass == kCoreEventClass && event.eventID == kAEOpenApplication
}
private func isReopenEvent(_ event: NSAppleEventDescriptor) -> Bool {
return event.eventClass == kCoreEventClass && event.eventID == kAEReopenApplication
}

View File

@ -25,12 +25,14 @@
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.</string>
<string>Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSBackgroundOnly</key>
<true/>
<key>com.wireguard.macos.app_id</key>
<string>$(APP_ID_MACOS)</string>
<key>com.wireguard.macos.app_group_id</key>
<string>$(DEVELOPMENT_TEAM).group.$(APP_ID_MACOS)</string>
</dict>
</plist>

View File

@ -4,5 +4,9 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>$(DEVELOPMENT_TEAM).group.$(APP_ID_MACOS)</string>
</array>
</dict>
</plist>

View File

@ -1,17 +1,36 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
#import <Cocoa/Cocoa.h>
int main(int argc, char *argv[])
{
NSString *appIdInfoDictionaryKey = @"com.wireguard.macos.app_id";
NSString *appId = [NSBundle.mainBundle objectForInfoDictionaryKey:appIdInfoDictionaryKey];
NSString *launchCode = @"LaunchedByWireGuardLoginItemHelper";
NSAppleEventDescriptor *paramDescriptor = [NSAppleEventDescriptor descriptorWithString:launchCode];
[NSWorkspace.sharedWorkspace launchAppWithBundleIdentifier:appId options:NSWorkspaceLaunchWithoutActivation
additionalEventParamDescriptor:paramDescriptor launchIdentifier:NULL];
NSString *appId = [NSBundle.mainBundle objectForInfoDictionaryKey:@"com.wireguard.macos.app_id"];
NSString *appGroupId = [NSBundle.mainBundle objectForInfoDictionaryKey:@"com.wireguard.macos.app_group_id"];
if (!appId || !appGroupId)
return 1;
NSURL *containerUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:appGroupId];
if (!containerUrl)
return 2;
uint64_t now = clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
if (![[NSData dataWithBytes:&now length:sizeof(now)] writeToURL:[containerUrl URLByAppendingPathComponent:@"login-helper-timestamp.bin"] atomically:YES])
return 3;
if (@available(macOS 10.15, *)) {
NSCondition *condition = [[NSCondition alloc] init];
NSURL *appURL = [NSWorkspace.sharedWorkspace URLForApplicationWithBundleIdentifier:appId];
if (!appURL)
return 4;
NSWorkspaceOpenConfiguration *openConfiguration = [NSWorkspaceOpenConfiguration configuration];
openConfiguration.activates = NO;
openConfiguration.addsToRecentItems = NO;
openConfiguration.hides = YES;
[NSWorkspace.sharedWorkspace openApplicationAtURL:appURL configuration:openConfiguration completionHandler:^(NSRunningApplication * _Nullable app, NSError * _Nullable error) {
[condition signal];
}];
[condition wait];
} else {
[NSWorkspace.sharedWorkspace launchAppWithBundleIdentifier:appId options:NSWorkspaceLaunchWithoutActivation
additionalEventParamDescriptor:NULL launchIdentifier:NULL];
}
return 0;
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -8,11 +8,12 @@ class MacAppStoreUpdateDetector {
guard isQuitEvent(quitAppleEvent) else { return false }
guard let senderPIDDescriptor = quitAppleEvent.attributeDescriptor(forKeyword: keySenderPIDAttr) else { return false }
let pid = senderPIDDescriptor.int32Value
wg_log(.debug, message: "aevt/quit Apple event received from pid: \(pid)")
guard let executablePath = getExecutablePath(from: pid) else { return false }
wg_log(.debug, message: "aevt/quit Apple event received from: \(executablePath)")
wg_log(.debug, message: "aevt/quit Apple event received from executable: \(executablePath)")
if executablePath.hasPrefix("/System/Library/") {
let executableName = URL(fileURLWithPath: executablePath, isDirectory: false).lastPathComponent
return executableName == "com.apple.CommerceKit.StoreAEService"
return executableName.hasPrefix("com.apple.") && executableName.hasSuffix(".StoreAEService")
}
return false
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import AppKit

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -7,7 +7,7 @@ extension NSTableView {
func dequeueReusableCell<T: NSView>() -> T {
let identifier = NSUserInterfaceItemIdentifier(NSStringFromClass(T.self))
if let cellView = makeView(withIdentifier: identifier, owner: self) {
//swiftlint:disable:next force_cast
// swiftlint:disable:next force_cast
return cellView as! T
}
let cellView = T()

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
protocol StatusMenuWindowDelegate: class {
protocol StatusMenuWindowDelegate: AnyObject {
func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?)
}
@ -14,8 +14,14 @@ class StatusMenu: NSMenu {
var statusMenuItem: NSMenuItem?
var networksMenuItem: NSMenuItem?
var deactivateMenuItem: NSMenuItem?
var firstTunnelMenuItemIndex = 0
var numberOfTunnelMenuItems = 0
private let tunnelsBreakdownMenu = NSMenu()
private let tunnelsMenuItem = NSMenuItem(title: tr("macTunnelsMenuTitle"), action: nil, keyEquivalent: "")
private let tunnelsMenuSeparatorItem = NSMenuItem.separator()
private var firstTunnelMenuItemIndex = 0
private var numberOfTunnelMenuItems = 0
private var tunnelsPresentationStyle = StatusMenuTunnelsPresentationStyle.inline
var currentTunnel: TunnelContainer? {
didSet {
@ -26,16 +32,20 @@ class StatusMenu: NSMenu {
init(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
super.init(title: tr("macMenuTitle"))
addStatusMenuItems()
addItem(NSMenuItem.separator())
tunnelsMenuItem.submenu = tunnelsBreakdownMenu
addItem(tunnelsMenuItem)
firstTunnelMenuItemIndex = numberOfItems
let isAdded = addTunnelMenuItems()
if isAdded {
addItem(NSMenuItem.separator())
}
populateInitialTunnelMenuItems()
addItem(tunnelsMenuSeparatorItem)
addTunnelManagementItems()
addItem(NSMenuItem.separator())
addApplicationItems()
@ -108,15 +118,6 @@ class StatusMenu: NSMenu {
deactivateMenuItem.isHidden = tunnel.status != .active
}
func addTunnelMenuItems() -> Bool {
let numberOfTunnels = tunnelsManager.numberOfTunnels()
for index in 0 ..< tunnelsManager.numberOfTunnels() {
let tunnel = tunnelsManager.tunnel(at: index)
insertTunnelMenuItem(for: tunnel, at: numberOfTunnelMenuItems)
}
return numberOfTunnels > 0
}
func addTunnelManagementItems() {
let manageItem = NSMenuItem(title: tr("macMenuManageTunnels"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
manageItem.target = self
@ -143,10 +144,20 @@ class StatusMenu: NSMenu {
@objc func tunnelClicked(sender: AnyObject) {
guard let tunnelMenuItem = sender as? TunnelMenuItem else { return }
if tunnelMenuItem.state == .off {
tunnelsManager.startActivation(of: tunnelMenuItem.tunnel)
let tunnel = tunnelMenuItem.tunnel
if tunnel.hasOnDemandRules {
let turnOn = !tunnel.isActivateOnDemandEnabled
tunnelsManager.setOnDemandEnabled(turnOn, on: tunnel) { error in
if error == nil && !turnOn {
self.tunnelsManager.startDeactivation(of: tunnel)
}
}
} else {
tunnelsManager.startDeactivation(of: tunnelMenuItem.tunnel)
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}
@ -166,34 +177,121 @@ class StatusMenu: NSMenu {
extension StatusMenu {
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
menuItem.target = self
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
if numberOfTunnelMenuItems == 0 {
insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
let nextNumberOfTunnels = numberOfTunnelMenuItems + 1
guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
return
}
numberOfTunnelMenuItems += 1
let menuItem = makeTunnelItem(tunnel: tunnel)
switch tunnelsPresentationStyle {
case .submenu:
tunnelsBreakdownMenu.insertItem(menuItem, at: tunnelIndex)
case .inline:
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
}
numberOfTunnelMenuItems = nextNumberOfTunnels
updateTunnelsMenuItemVisibility()
}
func removeTunnelMenuItem(at tunnelIndex: Int) {
removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
numberOfTunnelMenuItems -= 1
if numberOfTunnelMenuItems == 0 {
if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
removeItem(at: firstTunnelMenuItemIndex)
}
let nextNumberOfTunnels = numberOfTunnelMenuItems - 1
guard !reparentTunnelMenuItems(nextNumberOfTunnels: nextNumberOfTunnels) else {
return
}
switch tunnelsPresentationStyle {
case .submenu:
tunnelsBreakdownMenu.removeItem(at: tunnelIndex)
case .inline:
removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
}
numberOfTunnelMenuItems = nextNumberOfTunnels
updateTunnelsMenuItemVisibility()
}
func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
guard let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex) as? TunnelMenuItem else { return }
let oldMenuItemTunnel = oldMenuItem.tunnel
removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
let menuItem = TunnelMenuItem(tunnel: oldMenuItemTunnel, action: #selector(tunnelClicked(sender:)))
menuItem.target = self
insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
let tunnel = tunnelsManager.tunnel(at: newTunnelIndex)
let menuItem = makeTunnelItem(tunnel: tunnel)
switch tunnelsPresentationStyle {
case .submenu:
tunnelsBreakdownMenu.removeItem(at: oldTunnelIndex)
tunnelsBreakdownMenu.insertItem(menuItem, at: newTunnelIndex)
case .inline:
removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
}
}
private func makeTunnelItem(tunnel: TunnelContainer) -> TunnelMenuItem {
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
menuItem.target = self
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
return menuItem
}
private func populateInitialTunnelMenuItems() {
let numberOfTunnels = tunnelsManager.numberOfTunnels()
let initialStyle = tunnelsPresentationStyle.preferredPresentationStyle(numberOfTunnels: numberOfTunnels)
tunnelsPresentationStyle = initialStyle
switch initialStyle {
case .inline:
numberOfTunnelMenuItems = addTunnelMenuItems(into: self, at: firstTunnelMenuItemIndex)
case .submenu:
numberOfTunnelMenuItems = addTunnelMenuItems(into: tunnelsBreakdownMenu, at: 0)
}
updateTunnelsMenuItemVisibility()
}
private func reparentTunnelMenuItems(nextNumberOfTunnels: Int) -> Bool {
let nextStyle = tunnelsPresentationStyle.preferredPresentationStyle(numberOfTunnels: nextNumberOfTunnels)
switch (tunnelsPresentationStyle, nextStyle) {
case (.inline, .submenu):
tunnelsPresentationStyle = nextStyle
for index in (0..<numberOfTunnelMenuItems).reversed() {
removeItem(at: firstTunnelMenuItemIndex + index)
}
numberOfTunnelMenuItems = addTunnelMenuItems(into: tunnelsBreakdownMenu, at: 0)
updateTunnelsMenuItemVisibility()
return true
case (.submenu, .inline):
tunnelsPresentationStyle = nextStyle
tunnelsBreakdownMenu.removeAllItems()
numberOfTunnelMenuItems = addTunnelMenuItems(into: self, at: firstTunnelMenuItemIndex)
updateTunnelsMenuItemVisibility()
return true
case (.submenu, .submenu), (.inline, .inline):
return false
}
}
private func addTunnelMenuItems(into menu: NSMenu, at startIndex: Int) -> Int {
let numberOfTunnels = tunnelsManager.numberOfTunnels()
for tunnelIndex in 0..<numberOfTunnels {
let tunnel = tunnelsManager.tunnel(at: tunnelIndex)
let menuItem = makeTunnelItem(tunnel: tunnel)
menu.insertItem(menuItem, at: startIndex + tunnelIndex)
}
return numberOfTunnels
}
private func updateTunnelsMenuItemVisibility() {
switch tunnelsPresentationStyle {
case .inline:
tunnelsMenuItem.isHidden = true
case .submenu:
tunnelsMenuItem.isHidden = false
}
tunnelsMenuSeparatorItem.isHidden = numberOfTunnelMenuItems == 0
}
}
@ -203,6 +301,7 @@ class TunnelMenuItem: NSMenuItem {
private var statusObservationToken: AnyObject?
private var nameObservationToken: AnyObject?
private var isOnDemandEnabledObservationToken: AnyObject?
init(tunnel: TunnelContainer, action selector: Selector?) {
self.tunnel = tunnel
@ -215,7 +314,12 @@ class TunnelMenuItem: NSMenuItem {
let nameObservationToken = tunnel.observe(\TunnelContainer.name) { [weak self] _, _ in
self?.updateTitle()
}
let isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak self] _, _ in
self?.updateTitle()
self?.updateStatus()
}
self.statusObservationToken = statusObservationToken
self.isOnDemandEnabledObservationToken = isOnDemandEnabledObservationToken
self.nameObservationToken = nameObservationToken
}
@ -224,11 +328,35 @@ class TunnelMenuItem: NSMenuItem {
}
func updateTitle() {
title = tunnel.name
if tunnel.isActivateOnDemandEnabled {
title = tunnel.name + " (On-Demand)"
} else {
title = tunnel.name
}
}
func updateStatus() {
let shouldShowCheckmark = (tunnel.status != .inactive && tunnel.status != .deactivating)
state = shouldShowCheckmark ? .on : .off
if tunnel.isActivateOnDemandEnabled {
state = (tunnel.status == .inactive || tunnel.status == .deactivating) ? .mixed : .on
} else {
state = (tunnel.status == .inactive || tunnel.status == .deactivating) ? .off : .on
}
}
}
private enum StatusMenuTunnelsPresentationStyle {
case inline
case submenu
func preferredPresentationStyle(numberOfTunnels: Int) -> StatusMenuTunnelsPresentationStyle {
let maxInlineTunnels = 10
if case .inline = self, numberOfTunnels > maxInlineTunnels {
return .submenu
} else if case .submenu = self, numberOfTunnels <= maxInlineTunnels {
return .inline
} else {
return self
}
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -28,7 +28,9 @@ class ButtonRow: NSView {
}
var onButtonClicked: (() -> Void)?
var observationToken: AnyObject?
var statusObservationToken: AnyObject?
var isOnDemandEnabledObservationToken: AnyObject?
var hasOnDemandRulesObservationToken: AnyObject?
override var intrinsicContentSize: NSSize {
return NSSize(width: NSView.noIntrinsicMetric, height: button.intrinsicContentSize.height)
@ -62,6 +64,8 @@ class ButtonRow: NSView {
buttonTitle = ""
buttonToolTip = ""
onButtonClicked = nil
observationToken = nil
statusObservationToken = nil
isOnDemandEnabledObservationToken = nil
hasOnDemandRulesObservationToken = nil
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -16,7 +16,7 @@ class ConfTextStorage: NSTextStorage {
private(set) var hasError = false
private(set) var privateKeyString: String?
private(set) var hasOnePeer: Bool = false
private(set) var hasOnePeer = false
private(set) var lastOnePeerAllowedIPs = [String]()
private(set) var lastOnePeerDNSServers = [String]()
private(set) var lastOnePeerHasPublicKey = false
@ -67,7 +67,7 @@ class ConfTextStorage: NSTextStorage {
override func replaceCharacters(in range: NSRange, with str: String) {
beginEditing()
backingStore.replaceCharacters(in: range, with: str)
edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
edited(.editedCharacters, range: range, changeInLength: str.utf16.count - range.length)
endEditing()
}
@ -94,6 +94,7 @@ class ConfTextStorage: NSTextStorage {
func evaluateExcludePrivateIPs(highlightSpans: UnsafePointer<highlight_span>) {
var spans = highlightSpans
let string = backingStore.string
enum FieldType: String {
case dns
case allowedips
@ -102,7 +103,7 @@ class ConfTextStorage: NSTextStorage {
resetLastPeer()
while spans.pointee.type != HighlightEnd {
let span = spans.pointee
var substring = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string.lowercased()
var substring = String(string.substring(higlightSpan: span)).lowercased()
if span.type == HighlightError {
resetLastPeer()
@ -123,8 +124,9 @@ class ConfTextStorage: NSTextStorage {
let next = spans.successor()
let nextnext = next.successor()
if next.pointee.type == HighlightDelimiter && nextnext.pointee.type == HighlightCidr {
substring += backingStore.attributedSubstring(from: NSRange(location: next.pointee.start, length: next.pointee.len)).string +
backingStore.attributedSubstring(from: NSRange(location: nextnext.pointee.start, length: nextnext.pointee.len)).string
let delimiter = string.substring(higlightSpan: next.pointee)
let cidr = string.substring(higlightSpan: nextnext.pointee)
substring += delimiter + cidr
}
lastOnePeerAllowedIPs.append(substring)
} else if span.type == HighlightPublicKey {
@ -139,7 +141,8 @@ class ConfTextStorage: NSTextStorage {
hasError = false
privateKeyString = nil
let fullTextRange = NSRange(location: 0, length: (backingStore.string as NSString).length)
let string = backingStore.string
let fullTextRange = NSRange(..<string.endIndex, in: string)
backingStore.beginEditing()
let defaultAttributes: [NSAttributedString.Key: Any] = [
@ -147,15 +150,19 @@ class ConfTextStorage: NSTextStorage {
.font: defaultFont
]
backingStore.setAttributes(defaultAttributes, range: fullTextRange)
var spans = highlight_config(backingStore.string)!
var spans = highlight_config(string)!
evaluateExcludePrivateIPs(highlightSpans: spans)
let spansStart = spans
while spans.pointee.type != HighlightEnd {
let span = spans.pointee
let range = NSRange(location: span.start, length: span.len)
let startIndex = string.utf8.index(string.startIndex, offsetBy: span.start)
let endIndex = string.utf8.index(startIndex, offsetBy: span.len)
let range = NSRange(startIndex..<endIndex, in: string)
backingStore.setAttributes(nonColorAttributes(for: span.type), range: range)
let color = textColorTheme.colorMap[span.type.rawValue, default: textColorTheme.defaultColor]
backingStore.addAttribute(.foregroundColor, value: color, range: range)
@ -164,7 +171,7 @@ class ConfTextStorage: NSTextStorage {
}
if span.type == HighlightPrivateKey {
privateKeyString = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string
privateKeyString = String(string.substring(higlightSpan: span))
}
spans = spans.successor()
@ -178,3 +185,12 @@ class ConfTextStorage: NSTextStorage {
}
}
private extension String {
func substring(higlightSpan span: highlight_span) -> Substring {
let startIndex = self.utf8.index(self.utf8.startIndex, offsetBy: span.start)
let endIndex = self.utf8.index(startIndex, offsetBy: span.len)
return self[startIndex..<endIndex]
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -7,7 +7,7 @@ class ConfTextView: NSTextView {
private let confTextStorage = ConfTextStorage()
@objc dynamic var hasError: Bool = false
@objc dynamic var hasError = false
@objc dynamic var privateKeyString: String?
@objc dynamic var singlePeerAllowedIPs: [String]?
@ -70,7 +70,7 @@ class ConfTextView: NSTextView {
}
func setConfText(_ text: String) {
let fullTextRange = NSRange(location: 0, length: (string as NSString).length)
let fullTextRange = NSRange(..<string.endIndex, in: string)
if shouldChangeText(in: fullTextRange, replacementString: text) {
replaceCharacters(in: fullTextRange, with: text)
didChangeText()

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -49,7 +49,9 @@ class EditableKeyValueRow: NSView {
set(value) { valueImageView?.image = value }
}
var observationToken: AnyObject?
var statusObservationToken: AnyObject?
var isOnDemandEnabledObservationToken: AnyObject?
var hasOnDemandRulesObservationToken: AnyObject?
override var intrinsicContentSize: NSSize {
let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height)
@ -108,7 +110,9 @@ class EditableKeyValueRow: NSView {
key = ""
value = ""
isKeyInBold = false
observationToken = nil
statusObservationToken = nil
isOnDemandEnabledObservationToken = nil
hasOnDemandRulesObservationToken = nil
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
import CoreWLAN

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -12,9 +12,12 @@ class TunnelListRow: NSView {
self?.nameLabel.stringValue = tunnel.name
}
// Bind to the tunnel's status
statusImageView.image = TunnelListRow.image(for: tunnel?.status)
statusImageView.image = TunnelListRow.image(for: tunnel)
statusObservationToken = tunnel?.observe(\TunnelContainer.status) { [weak self] tunnel, _ in
self?.statusImageView.image = TunnelListRow.image(for: tunnel.status)
self?.statusImageView.image = TunnelListRow.image(for: tunnel)
}
isOnDemandEnabledObservationToken = tunnel?.observe(\TunnelContainer.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
self?.statusImageView.image = TunnelListRow.image(for: tunnel)
}
}
}
@ -33,6 +36,7 @@ class TunnelListRow: NSView {
private var statusObservationToken: AnyObject?
private var nameObservationToken: AnyObject?
private var isOnDemandEnabledObservationToken: AnyObject?
init() {
super.init(frame: CGRect.zero)
@ -56,15 +60,19 @@ class TunnelListRow: NSView {
fatalError("init(coder:) has not been implemented")
}
static func image(for status: TunnelStatus?) -> NSImage? {
guard let status = status else { return nil }
switch status {
static func image(for tunnel: TunnelContainer?) -> NSImage? {
guard let tunnel = tunnel else { return nil }
switch tunnel.status {
case .active, .restarting, .reasserting:
return NSImage(named: NSImage.statusAvailableName)
case .activating, .waiting, .deactivating:
return NSImage(named: NSImage.statusPartiallyAvailableName)
case .inactive:
return NSImage(named: NSImage.statusNoneName)
if tunnel.isActivateOnDemandEnabled {
return NSImage(named: NSImage.Name.statusOnDemandEnabled)
} else {
return NSImage(named: NSImage.statusNoneName)
}
}
}
@ -73,3 +81,7 @@ class TunnelListRow: NSView {
statusImageView.image = nil
}
}
extension NSImage.Name {
static let statusOnDemandEnabled = NSImage.Name("StatusCircleYellow")
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -18,6 +18,9 @@ class LogViewController: NSViewController {
}
}
private var boundsChangedNotificationToken: NotificationToken?
private var frameChangedNotificationToken: NotificationToken?
let scrollView: NSScrollView = {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
@ -104,13 +107,13 @@ class LogViewController: NSViewController {
clipView.documentView = tableView
scrollView.contentView = clipView
_ = NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: clipView, queue: OperationQueue.main) { [weak self] _ in
boundsChangedNotificationToken = NotificationCenter.default.observe(name: NSView.boundsDidChangeNotification, object: clipView, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
let lastVisibleRowIndex = self.tableView.row(at: NSPoint(x: 0, y: self.scrollView.contentView.documentVisibleRect.maxY - 1))
self.isInScrolledToEndMode = lastVisibleRowIndex < 0 || lastVisibleRowIndex == self.logEntries.count - 1
}
_ = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: tableView, queue: OperationQueue.main) { [weak self] _ in
frameChangedNotificationToken = NotificationCenter.default.observe(name: NSView.frameDidChangeNotification, object: tableView, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
if self.isInScrolledToEndMode {
DispatchQueue.main.async {

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
@ -216,10 +216,19 @@ class TunnelDetailTableViewController: NSViewController {
}
@objc func handleToggleActiveStatusAction() {
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
if tunnel.hasOnDemandRules {
let turnOn = !tunnel.isActivateOnDemandEnabled
tunnelsManager.setOnDemandEnabled(turnOn, on: tunnel) { error in
if error == nil && !turnOn {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
} else {
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}
@ -422,79 +431,113 @@ extension TunnelDetailTableViewController: NSTableViewDelegate {
func statusCell() -> NSView {
let cell: KeyValueImageRow = tableView.dequeueReusableCell()
cell.key = tr(format: "macFieldKey (%@)", tr("tunnelInterfaceStatus"))
cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
guard let cell = cell else { return }
cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
cell.value = TunnelDetailTableViewController.localizedStatusDescription(for: tunnel)
cell.valueImage = TunnelDetailTableViewController.image(for: tunnel)
}
cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
return cell
}
func toggleStatusCell() -> NSView {
let cell: ButtonRow = tableView.dequeueReusableCell()
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
cell.buttonToolTip = tr("macToolTipToggleStatus")
cell.onButtonClicked = { [weak self] in
self?.handleToggleActiveStatusAction()
}
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
let changeHandler: (TunnelContainer, Any) -> Void = { [weak cell] tunnel, _ in
guard let cell = cell else { return }
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(for: tunnel)
cell.isButtonEnabled = (tunnel.hasOnDemandRules || tunnel.status == .active || tunnel.status == .inactive)
}
cell.statusObservationToken = tunnel.observe(\.status, changeHandler: changeHandler)
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled, changeHandler: changeHandler)
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules, changeHandler: changeHandler)
return cell
}
private static func localizedStatusDescription(forStatus status: TunnelStatus) -> String {
private static func localizedStatusDescription(for tunnel: TunnelContainer) -> String {
let status = tunnel.status
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
var text: String
switch status {
case .inactive:
return tr("tunnelStatusInactive")
text = tr("tunnelStatusInactive")
case .activating:
return tr("tunnelStatusActivating")
text = tr("tunnelStatusActivating")
case .active:
return tr("tunnelStatusActive")
text = tr("tunnelStatusActive")
case .deactivating:
return tr("tunnelStatusDeactivating")
text = tr("tunnelStatusDeactivating")
case .reasserting:
return tr("tunnelStatusReasserting")
text = tr("tunnelStatusReasserting")
case .restarting:
return tr("tunnelStatusRestarting")
text = tr("tunnelStatusRestarting")
case .waiting:
return tr("tunnelStatusWaiting")
text = tr("tunnelStatusWaiting")
}
if tunnel.hasOnDemandRules {
text += isOnDemandEngaged ?
tr("tunnelStatusAddendumOnDemandEnabled") : tr("tunnelStatusAddendumOnDemandDisabled")
}
return text
}
private static func image(forStatus status: TunnelStatus?) -> NSImage? {
guard let status = status else { return nil }
switch status {
private static func image(for tunnel: TunnelContainer?) -> NSImage? {
guard let tunnel = tunnel else { return nil }
switch tunnel.status {
case .active, .restarting, .reasserting:
return NSImage(named: NSImage.statusAvailableName)
case .activating, .waiting, .deactivating:
return NSImage(named: NSImage.statusPartiallyAvailableName)
case .inactive:
return NSImage(named: NSImage.statusNoneName)
if tunnel.isActivateOnDemandEnabled {
return NSImage(named: NSImage.Name.statusOnDemandEnabled)
} else {
return NSImage(named: NSImage.statusNoneName)
}
}
}
private static func localizedToggleStatusActionText(forStatus status: TunnelStatus) -> String {
switch status {
case .waiting:
return tr("macToggleStatusButtonWaiting")
case .inactive:
return tr("macToggleStatusButtonActivate")
case .activating:
return tr("macToggleStatusButtonActivating")
case .active:
return tr("macToggleStatusButtonDeactivate")
case .deactivating:
return tr("macToggleStatusButtonDeactivating")
case .reasserting:
return tr("macToggleStatusButtonReasserting")
case .restarting:
return tr("macToggleStatusButtonRestarting")
private static func localizedToggleStatusActionText(for tunnel: TunnelContainer) -> String {
if tunnel.hasOnDemandRules {
let turnOn = !tunnel.isActivateOnDemandEnabled
if turnOn {
return tr("macToggleStatusButtonEnableOnDemand")
} else {
if tunnel.status == .active {
return tr("macToggleStatusButtonDisableOnDemandDeactivate")
} else {
return tr("macToggleStatusButtonDisableOnDemand")
}
}
} else {
switch tunnel.status {
case .waiting:
return tr("macToggleStatusButtonWaiting")
case .inactive:
return tr("macToggleStatusButtonActivate")
case .activating:
return tr("macToggleStatusButtonActivating")
case .active:
return tr("macToggleStatusButtonDeactivate")
case .deactivating:
return tr("macToggleStatusButtonDeactivating")
case .reasserting:
return tr("macToggleStatusButtonReasserting")
case .restarting:
return tr("macToggleStatusButtonRestarting")
}
}
}
}

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
protocol TunnelEditViewControllerDelegate: class {
protocol TunnelEditViewControllerDelegate: AnyObject {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}

View File

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
protocol TunnelsListTableViewControllerDelegate: class {
protocol TunnelsListTableViewControllerDelegate: AnyObject {
func tunnelsSelected(tunnelIndices: [Int])
func tunnelsListEmpty()
}
@ -232,10 +232,19 @@ class TunnelsListTableViewController: NSViewController {
let tunnelIndex = tableView.clickedRow
guard tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() else { return }
let tunnel = tunnelsManager.tunnel(at: tunnelIndex)
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
if tunnel.hasOnDemandRules {
let turnOn = !tunnel.isActivateOnDemandEnabled
tunnelsManager.setOnDemandEnabled(turnOn, on: tunnel) { error in
if error == nil && !turnOn {
self.tunnelsManager.startDeactivation(of: tunnel)
}
}
} else {
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error {
typealias AlertText = (title: String, message: String)

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> {
case success(_ value: T)

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation

View File

@ -95,3 +95,181 @@
"macMenuManageTunnels" = "Gestiona túnels";
"macMenuCut" = "Talla";
"macMenuCopy" = "Copia";
"newTunnelViewTitle" = "New configuration";
"macMenuDeleteSelected" = "Delete Selected";
"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
"tunnelPeerEndpoint" = "Endpoint";
"tunnelInterfaceMTU" = "MTU";
"alertInvalidInterfaceMessageListenPortInvalid" = "Interfaces listen port must be between 0 and 65535, or unspecified";
"addPeerButtonTitle" = "Add peer";
"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
"macMenuTitle" = "WireGuard";
"macAlertNoInterface" = "Configuration must have an Interface section.";
"macNameFieldExportZip" = "Export tunnels to:";
"editTunnelViewTitle" = "Edit configuration";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
"macEditDiscard" = "Discard";
"macSheetButtonExportZip" = "Save";
"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
"tunnelsListTitle" = "WireGuard";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"macMenuExportTunnels" = "Export Tunnels to Zip…";
"macMenuShowAllApps" = "Show All";
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
"macMenuHideApp" = "Hide WireGuard";
"macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Deleting…";
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
"macButtonEdit" = "Edit";
"macAlertPublicKeyInvalid" = "Public key is invalid";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"macNameFieldExportLog" = "Save log to:";
"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
"tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: PrivateKey, ListenPort, Address, DNS and MTU.";
"macLogColumnTitleTime" = "Time";
"actionOK" = "OK";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertInvalidInterfaceMessageMTUInvalid" = "Interfaces MTU must be between 576 and 65535, or unspecified";
"tunnelOnDemandWiFi" = "Wi-Fi";
"alertTunnelNameEmptyTitle" = "No name provided";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
"macToggleStatusButtonActivating" = "Activating…";
"macMenuQuit" = "Quit WireGuard";
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
"tunnelStatusDeactivating" = "Deactivating";
"alertInvalidInterfaceTitle" = "Invalid interface";
"tunnelSectionTitleStatus" = "Status";
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
"alertTunnelActivationFailureTitle" = "Activation failure";
"macLogButtonTitleClose" = "Close";
"tunnelOnDemandSSIDViewTitle" = "SSIDs";
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
"tunnelEditPlaceholderTextOptional" = "Optional";
"settingsExportZipButtonTitle" = "Export zip archive";
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
"tunnelEditPlaceholderTextAutomatic" = "Automatic";
"macViewPrivateData" = "view tunnel private keys";
"alertInvalidPeerMessageEndpointInvalid" = "Peers endpoint must be of the form host:port or [host]:port";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
"addTunnelMenuImportFile" = "Create from file or archive";
"deletePeerConfirmationAlertButtonTitle" = "Delete";
"addTunnelMenuQRCode" = "Create from QR code";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peers preshared key must be a 32-byte key in base64 encoding";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"macMenuEdit" = "Edit";
"donateLink" = "♥ Donate to the WireGuard Project";
"macMenuWindow" = "Window";
"tunnelStatusRestarting" = "Restarting";
"tunnelHandshakeTimestampNow" = "Now";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"tunnelInterfaceListenPort" = "Listen port";
"tunnelOnDemandOptionEthernetOnly" = "Ethernet only";
"macMenuHideOtherApps" = "Hide Others";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interfaces private key must be a 32-byte key in base64 encoding";
"deleteTunnelButtonTitle" = "Delete tunnel";
"alertInvalidInterfaceMessageDNSInvalid" = "Interfaces DNS servers must be a list of comma-separated IP addresses";
"tunnelStatusInactive" = "Inactive";
"macAlertPrivateKeyInvalid" = "Private key is invalid.";
"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peers persistent keepalive must be between 0 to 65535, or unspecified";
"macMenuNetworksNone" = "Networks: None";
"tunnelOnDemandSSIDsKey" = "SSIDs";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"macMenuSelectAll" = "Select All";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peers public key must be a 32-byte key in base64 encoding";
"tunnelOnDemandCellular" = "Cellular";
"macConfirmAndQuitAlertQuitWireGuard" = "Quit WireGuard";
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
"macFieldOnDemand" = "On-Demand:";
"macMenuCloseWindow" = "Close Window";
"macSheetButtonExportLog" = "Save";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"macMenuEditTunnel" = "Edit…";
"macButtonImportTunnels" = "Import tunnel(s) from file";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"tunnelPeerPreSharedKey" = "Preshared key";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
"macLogButtonTitleSave" = "Save…";
"macMenuToggleStatus" = "Toggle Status";
"macMenuMinimize" = "Minimize";
"deletePeerButtonTitle" = "Delete peer";
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
"macMenuPaste" = "Paste";
"macAlertMultipleInterfaces" = "Configuration must have only one Interface section.";
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
"macUnusableTunnelMessage" = "The configuration for this tunnel cannot be found in the keychain.";
"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
"macMenuZoom" = "Zoom";
"alertBadArchiveTitle" = "Unable to read zip archive";
"macExportPrivateData" = "export tunnel private keys";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
"tunnelPeerLastHandshakeTime" = "Latest handshake";
"macAlertPreSharedKeyInvalid" = "Preshared key is invalid";
"alertBadConfigImportTitle" = "Unable to import tunnel";
"macEditSave" = "Save";
"macConfirmAndQuitAlertCloseWindow" = "Close Tunnels Manager";
"macMenuFile" = "File";
"tunnelStatusActivating" = "Activating";
"macToolTipToggleStatus" = "Toggle status (⌘T)";
"macTunnelsMenuTitle" = "Tunnels";
"alertTunnelActivationSystemErrorTitle" = "Activation failure";
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interfaces private key is required";
"alertNoTunnelsToExportTitle" = "Nothing to export";
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
"macMenuImportTunnels" = "Import Tunnel(s) from File…";
"macMenuViewLog" = "View Log";
"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: PublicKey, PresharedKey, AllowedIPs, Endpoint and PersistentKeepalive";
"tunnelOnDemandNoSSIDs" = "No SSIDs";
"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
"tunnelEditPlaceholderTextOff" = "Off";
"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
"macUnusableTunnelButtonTitleDeleteTunnel" = "Delete tunnel";
"tunnelEditPlaceholderTextRequired" = "Required";
"tunnelStatusReasserting" = "Reactivating";
"macMenuTunnel" = "Tunnel";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"macLogColumnTitleLogMessage" = "Log message";
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
"macMenuAbout" = "About WireGuard";
"macSheetButtonImport" = "Import";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"macFieldOnDemandSSIDs" = "SSIDs:";
"deletePeerConfirmationAlertMessage" = "Delete this peer?";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"tunnelStatusActive" = "Active";
"tunnelStatusWaiting" = "Waiting";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
"addTunnelMenuFromScratch" = "Create from scratch";
"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi or ethernet";
"macToggleStatusButtonActivate" = "Activate";
"macAlertNameIsEmpty" = "Name is required";

View File

@ -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
@ -255,8 +255,6 @@
"alertTunnelActivationFileDescriptorFailureMessage" = "TUN Dateideskriptor kann nicht ermittelt werden.";
"alertTunnelActivationSetNetworkSettingsMessage" = "Netzwerkeinstellungen können nicht auf Tunnelobjekt angewendet werden.";
"alertTunnelActivationFailureOnDemandAddendum" = " Dieser Tunnel hat 'Activate on Demand' aktiviert, so dass dieser Tunnel vom Betriebssystem automatisch aktiviert werden kann. Du kannst 'Activate on Demand' in dieser App deaktivieren, indem du die Tunnelkonfiguration änderst.";
"alertTunnelDNSFailureTitle" = "DNS-Auflösungsfehler";
"alertTunnelDNSFailureMessage" = "Eine oder mehrere Endpunkt-Domains konnten nicht aufgelöst werden.";
@ -443,3 +441,4 @@
// Donation
"donateLink" = "♥ Spende an das WireGuard Projekt";
"macTunnelsMenuTitle" = "Tunnels";

View File

@ -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
@ -80,7 +80,7 @@
"tunnelPeerPublicKey" = "Clave pública";
"tunnelPeerPreSharedKey" = "Clave precompartida";
"tunnelPeerEndpoint" = "Punto final";
"tunnelPeerPersistentKeepalive" = "Mantenimiento persistente";
"tunnelPeerPersistentKeepalive" = "Keepalive persistente";
"tunnelPeerAllowedIPs" = "IPs permitidas";
"tunnelPeerRxBytes" = "Datos recibidos";
"tunnelPeerTxBytes" = "Datos enviados";
@ -177,15 +177,218 @@
"alertInvalidPeerMessagePreSharedKeyInvalid" = "La clave precompartida del par debe ser de 32 bytes en codificación base64";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Las IPs permitidas del par deben ser una lista de direcciones IP separadas por comas, opcionalmente en notación CIDR";
"alertInvalidPeerMessageEndpointInvalid" = "El punto final del par debe ser de la forma host:port o [host]:port";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "El keepalive persistente del par debe estar entre 0 y 65535, o no especificado";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "El keepalive persistente del peer debe estar entre 0 y 65535, o no especificado";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Dos o mas pares no pueden tener la misma llave pùblica";
// Scanning QR code UI
"scanQRCodeViewTitle" = "Escanear código QR";
"scanQRCodeTipText" = "Consejo: generar con `qrencode -t ansiutf8 tunnel.conf`";
// Scanning QR code alerts
"alertScanQRCodeCameraUnsupportedTitle" = "Cámara no soportada";
"alertScanQRCodeCameraUnsupportedMessage" = "Este dispositivo no es capaz de escanear códigos QR";
"alertScanQRCodeInvalidQRCodeTitle" = "Código QR inválido";
"alertScanQRCodeInvalidQRCodeMessage" = "El código QR escaneado no es una configuración válida de WireGuard";
"alertScanQRCodeUnreadableQRCodeTitle" = "Código inválido";
"alertScanQRCodeUnreadableQRCodeMessage" = "El código escaneado no pudo ser leído";
"alertScanQRCodeNamePromptTitle" = "Por favor, nombra el túnel escaneado";
// Settings UI
"settingsViewTitle" = "Configuración";
"settingsSectionTitleAbout" = "Acerca de";
"settingsVersionKeyWireGuardForIOS" = "WireGuard para iOS";
"settingsSectionTitleExportConfigurations" = "Exportar configuraciones";
"settingsExportZipButtonTitle" = "Exportar archivo zip";
"settingsSectionTitleTunnelLog" = "Registro";
"settingsViewLogButtonTitle" = "Ver registro";
// Log view
"logViewTitle" = "Registro";
// Log alerts
"alertUnableToRemovePreviousLogTitle" = "Exportación de registros fallida";
"alertUnableToRemovePreviousLogMessage" = "El registro preexistente no ha podido ser borrado";
"alertUnableToWriteLogTitle" = "Exportación de registros fallida";
"alertUnableToWriteLogMessage" = "No se pudo escribir en el archivo de registros";
// Zip import / export error alerts
"alertCantOpenInputZipFileTitle" = "No se pudo leer el archivo zip";
"alertCantOpenInputZipFileMessage" = "El archivo zip no pudo ser leído.";
"alertCantOpenOutputZipFileForWritingTitle" = "No se pudo crear el archivo zip";
"alertCantOpenOutputZipFileForWritingMessage" = "No se pudo abrir el archivo zip para escribir.";
"alertBadArchiveTitle" = "No se pudo leer el archivo zip";
"alertBadArchiveMessage" = "Archivo zip erróneo o corrupto.";
"alertNoTunnelsToExportTitle" = "Nada para exportar";
"alertNoTunnelsToExportMessage" = "No hay túneles para exportar";
"alertNoTunnelsInImportedZipArchiveTitle" = "No hay túneles en el archivo zip";
// Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Fallo en la activación";
"alertTunnelActivationFailureMessage" = "El túnel no pudo ser activado. Por favor, asegúrese de estar conectado a Internet.";
"alertTunnelActivationSavedConfigFailureMessage" = "No se ha podido recuperar la información del túnel de la configuración guardada.";
"alertTunnelDNSFailureTitle" = "Fallo en resolución DNS";
"alertSystemErrorOnAddTunnelTitle" = "No se pudo crear el túnel";
"alertSystemErrorOnModifyTunnelTitle" = "No se pudo modificar el túnel";
"alertSystemErrorOnRemoveTunnelTitle" = "No se pudo eliminar el túnel";
/* The alert message for this alert shall include
one of the alertSystemErrorMessage* listed further down */
"alertTunnelActivationSystemErrorTitle" = "Fallo en la activación";
"alertTunnelActivationSystemErrorMessage (%@)" = "No se pudo activar el túnel. %@";
/* alertSystemErrorMessage* messages */
"alertSystemErrorMessageTunnelConfigurationInvalid" = "La configuración es inválida.";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "La configuración está desactivada.";
"alertSystemErrorMessageTunnelConnectionFailed" = "La conexión ha fallado.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Error desconocido de sistema.";
"macMenuTitle" = "WireGuard";
"macMenuManageTunnels" = "Gestionar túneles";
"macMenuImportTunnels" = "Importar túnel(es) desde archivo";
"macMenuViewLog" = "Ver registro";
"macMenuAbout" = "Acerca de WireGuard";
"macMenuQuit" = "Salir de WireGuard";
"macMenuHideApp" = "Ocultar WireGuard";
"macMenuShowAllApps" = "Mostrar todo";
"macMenuFile" = "Archivo";
"macMenuCloseWindow" = "Cerrar Ventana";
"macMenuEdit" = "Editar";
"macMenuCut" = "Cortar";
"macMenuCopy" = "Copiar";
"macMenuPaste" = "Pegar";
"macMenuSelectAll" = "Seleccionar todo";
"macMenuTunnel" = "Túnel";
"macMenuToggleStatus" = "Cambiar estado";
"macMenuEditTunnel" = "Editar…";
"macMenuDeleteSelected" = "Eliminar elementos seleccionados";
"macMenuWindow" = "Ventana";
"macMenuMinimize" = "Minimizar";
// Mac manage tunnels window
"macWindowTitleManageTunnels" = "Gestionar Túneles WireGuard";
"macDeleteTunnelConfirmationAlertMessage (%@)" = "¿Estás seguro que deseas eliminar \"%@\"?";
"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "¿Está seguro que desea eliminar %d túneles?";
"macDeleteTunnelConfirmationAlertInfo" = "No puedes deshacer esta acción.";
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Eliminar";
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancelar";
"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Eliminando…";
"macButtonImportTunnels" = "Importar túnel(es) desde archivo";
"macSheetButtonImport" = "Importar";
"macNameFieldExportLog" = "Guardar registro en:";
"macSheetButtonExportLog" = "Guardar";
"macNameFieldExportZip" = "Exportar túneles a:";
"macSheetButtonExportZip" = "Guardar";
"macButtonDeleteTunnels (%d)" = "Eliminar %d túneles";
"macButtonEdit" = "Editar";
"macFieldOnDemand" = "Bajo demanda:";
"macFieldOnDemandSSIDs" = "SSIDs:";
// Mac status display
"macStatus (%@)" = "Estado: %@";
// Mac editing config
"macEditDiscard" = "Descartar";
"macEditSave" = "Guardar";
"macAlertDNSInvalid (%@)" = "El DNS %@ no es válido.";
"macAlertPublicKeyInvalid" = "La Clave pública no es válida";
"macAlertPreSharedKeyInvalid" = "La clave compartida no es válida";
"macAlertEndpointInvalid (%@)" = "Endpoint %@ no es válido";
"macAlertPersistentKeepliveInvalid (%@)" = "El valor keepalive persistente '%@' no es válido";
"macAlertInfoUnrecognizedPeerKey" = "Las claves válidas son: PublicKey, PresharedKey, AllowedIPs, Endpoint y PersistentKeepalive";
"macConfirmAndQuitAlertQuitWireGuard" = "Salir de WireGuard";
"macConfirmAndQuitAlertCloseWindow" = "Cerrar Gestor de túneles";
// Mac tooltip
"macToolTipEditTunnel" = "Editar túnel (⌘E)";
"macToolTipToggleStatus" = "Cambiar estado (⌘T)";
// Mac log view
"macLogColumnTitleTime" = "Tiempo";
"macLogColumnTitleLogMessage" = "Mensaje de registro";
"macLogButtonTitleClose" = "Cerrar";
"macLogButtonTitleSave" = "Guardar…";
// Mac unusable tunnel view
"macUnusableTunnelMessage" = "La configuración de este túnel no se encuentra en el llavero.";
"macUnusableTunnelButtonTitleDeleteTunnel" = "Eliminar túnel";
// Mac App Store updating alert
"macAppStoreUpdatingAlertMessage" = "App Store desea actualizar WireGuard";
// Donation
"donateLink" = "♥ Donar al Proyecto WireGuard";
"macAlertNoInterface" = "Configuration must have an Interface section.";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"macMenuExportTunnels" = "Export Tunnels to Zip…";
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: PrivateKey, ListenPort, Address, DNS and MTU.";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertTunnelNameEmptyTitle" = "No name provided";
"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
"macViewPrivateData" = "view tunnel private keys";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"macMenuHideOtherApps" = "Hide Others";
"macAlertPrivateKeyInvalid" = "Private key is invalid.";
"macMenuNetworksNone" = "Networks: None";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
"macAlertMultipleInterfaces" = "Configuration must have only one Interface section.";
"macMenuZoom" = "Zoom";
"macExportPrivateData" = "export tunnel private keys";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
"macTunnelsMenuTitle" = "Tunnels";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
"macAlertNameIsEmpty" = "Name is required";

View File

@ -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
@ -347,3 +347,58 @@
// Donation
"donateLink" = "♥ کمک مالی به پروژه WireGuard";
"alertInvalidInterfaceMessageListenPortInvalid" = "Interfaces listen port must be between 0 and 65535, or unspecified";
"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
"macAlertNoInterface" = "Configuration must have an Interface section.";
"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: PrivateKey, ListenPort, Address, DNS and MTU.";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertInvalidInterfaceMessageMTUInvalid" = "Interfaces MTU must be between 576 and 65535, or unspecified";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
"alertInvalidPeerMessageEndpointInvalid" = "Peers endpoint must be of the form host:port or [host]:port";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peers preshared key must be a 32-byte key in base64 encoding";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interfaces private key must be a 32-byte key in base64 encoding";
"alertInvalidInterfaceMessageDNSInvalid" = "Interfaces DNS servers must be a list of comma-separated IP addresses";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peers persistent keepalive must be between 0 to 65535, or unspecified";
"alertInvalidPeerMessagePublicKeyRequired" = "Peers public key is required";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peers public key must be a 32-byte key in base64 encoding";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
"macMenuToggleStatus" = "Toggle Status";
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
"macAlertMultipleInterfaces" = "Configuration must have only one Interface section.";
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
"macUnusableTunnelMessage" = "The configuration for this tunnel cannot be found in the keychain.";
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
"macToolTipToggleStatus" = "Toggle status (⌘T)";
"macTunnelsMenuTitle" = "Tunnels";
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: PublicKey, PresharedKey, AllowedIPs, Endpoint and PersistentKeepalive";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peers allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";

View File

@ -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
@ -12,20 +12,103 @@
"tunnelsListTitle" = "WireGuard";
"tunnelsListSettingsButtonTitle" = "Asetukset";
"tunnelsListCenteredAddTunnelButtonTitle" = "Lisää tunneli";
"tunnelsListSwipeDeleteButtonTitle" = "Poista";
"tunnelsListSelectButtonTitle" = "Valitse";
"tunnelsListSelectAllButtonTitle" = "Valitse kaikki";
"tunnelsListDeleteButtonTitle" = "Poista";
"tunnelsListSelectedTitle (%d)" = "%d poistettu";
// Tunnels list menu
"addTunnelMenuHeader" = "Lisää uusi WireGuard tunneli";
"addTunnelMenuImportFile" = "Luo tiedostosta tai arkistosta";
"addTunnelMenuQRCode" = "Luo QR-koodista";
"addTunnelMenuFromScratch" = "Luo alusta";
// Tunnels list alerts
"alertImportedFromMultipleFilesTitle (%d)" = "Luotu %d tunnelia";
"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Luotu %1$d / %2$d tunnelia tuoduista tiedostoista";
"alertImportedFromZipTitle (%d)" = "Luotu %d tunnelia";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Luotu %1$d / %2$d tunnelia zip-arkistosta";
"alertBadConfigImportTitle" = "Tunnelia ei voitu tuoda";
"alertBadConfigImportMessage (%@)" = "Tiedosto ”%@” ei sisällä kelvollista WireGuard konfiguraatiota";
"deleteTunnelsConfirmationAlertButtonTitle" = "Poista";
"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Poista %d tunneli?";
"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Poista %d tunnelit?";
// Tunnel detail and edit UI
"newTunnelViewTitle" = "Uusi konfiguraatio";
"editTunnelViewTitle" = "Muokkaa konfiguraatiota";
"tunnelSectionTitleStatus" = "Tila";
"tunnelStatusInactive" = "Ei aktiivinen";
"tunnelStatusActivating" = "Aktivoidaan";
"tunnelStatusActive" = "Aktiivinen";
"tunnelStatusDeactivating" = "Deaktivoidaan";
"tunnelStatusReasserting" = "Aktivoidaan uudelleen";
"tunnelStatusRestarting" = "Käynnistetään uudelleen";
"tunnelStatusWaiting" = "Odotetaan";
"macToggleStatusButtonActivate" = "Aktivoi";
"macToggleStatusButtonActivating" = "Aktivoidaan…";
"macToggleStatusButtonDeactivate" = "Deaktivoi";
"macToggleStatusButtonDeactivating" = "Deaktivoidaan…";
"macToggleStatusButtonReasserting" = "Aktivoidaan uudelleen…";
"macToggleStatusButtonRestarting" = "Käynnistetään uudelleen…";
"macToggleStatusButtonWaiting" = "Odotetaan…";
"tunnelSectionTitleInterface" = "Liittymä";
"tunnelInterfaceName" = "Nimi";
"tunnelInterfacePrivateKey" = "Yksityinen avain";
"tunnelInterfacePublicKey" = "Julkinen avain";
"tunnelInterfaceGenerateKeypair" = "Luo avainpari";
"tunnelInterfaceAddresses" = "Osoitteet";
"tunnelInterfaceListenPort" = "Kuuntele porttia";
"tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS palvelimet";
"tunnelInterfaceStatus" = "Tila";
"tunnelSectionTitlePeer" = "Osapuoli";
"tunnelPeerPublicKey" = "Julkinen avain";
"tunnelPeerPreSharedKey" = "Jaettu avain";
"tunnelPeerEndpoint" = "Päätepiste";
"tunnelPeerPersistentKeepalive" = "Jatkuva keepalive";
"tunnelPeerAllowedIPs" = "Sallitut IP-osoitteet";
"tunnelPeerRxBytes" = "Data vastaanotettu";
"tunnelPeerTxBytes" = "Data lähetetty";
"tunnelPeerLastHandshakeTime" = "Viimeisin kättely";
"tunnelPeerExcludePrivateIPs" = "Jätä pois yksityiset IP-osoitteet";
"tunnelSectionTitleOnDemand" = "Automaattinen käyttöönotto";
"tunnelOnDemandCellular" = "Matkapuhelinverkko";
"tunnelOnDemandEthernet" = "Ethernet";
"tunnelOnDemandWiFi" = "Wi-Fi";
"tunnelOnDemandSSIDsKey" = "SSID:t";
"tunnelOnDemandAnySSID" = "Mikä tahansa SSID";
"tunnelOnDemandOnlyTheseSSIDs" = "Vain nämä SSID:t";
"tunnelOnDemandExceptTheseSSIDs" = "Poislukien SSID:t";
"tunnelOnDemandOnlySSID (%d)" = "Vain %d SSID";
"tunnelOnDemandOnlySSIDs (%d)" = "Vain %d SSID:t";
"tunnelOnDemandExceptSSID (%d)" = "Kaikki paitsi %d SSID";
"tunnelOnDemandExceptSSIDs (%d)" = "Kaikki paitsi %d SSID:t";
"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
"tunnelOnDemandSSIDViewTitle" = "SSID:t";
"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSID:t";
"tunnelOnDemandNoSSIDs" = "Ei SSID-tietoja";
"tunnelOnDemandSectionTitleAddSSIDs" = "Lisää SSID-tietoja";
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Lisää yhdistetty: %@";
"tunnelOnDemandAddMessageAddNewSSID" = "Lisää uusi";
"tunnelOnDemandKey" = "Tarvittaessa";
"tunnelOnDemandOptionOff" = "Pois päältä";
@ -35,6 +118,12 @@
"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi tai Ethernet";
"tunnelOnDemandOptionEthernetOnly" = "Vain Ethernet";
"addPeerButtonTitle" = "Lisää toinen osapuoli";
"deletePeerButtonTitle" = "Poista osapuoli";
"deletePeerConfirmationAlertButtonTitle" = "Poista";
"deletePeerConfirmationAlertMessage" = "Poistetaanko tämä osapuoli?";
"deleteTunnelButtonTitle" = "Poista tunneli";
"deleteTunnelConfirmationAlertButtonTitle" = "Poista";
"deleteTunnelConfirmationAlertMessage" = "Poistetaanko tämä tunneli?";
@ -42,5 +131,272 @@
"tunnelEditPlaceholderTextRequired" = "Pakollinen";
"tunnelEditPlaceholderTextOptional" = "Valinnainen";
"tunnelEditPlaceholderTextAutomatic" = "Automaattinen";
"tunnelEditPlaceholderTextStronglyRecommended" = "Erittäin suositeltavaa";
"tunnelEditPlaceholderTextOff" = "Pois päältä";
"tunnelPeerPersistentKeepaliveValue (%@)" = "joka %@ sekuntin välein";
"tunnelHandshakeTimestampNow" = "Nyt";
"tunnelHandshakeTimestampSystemClockBackward" = "(Järjestelmän kello jättää)";
"tunnelHandshakeTimestampAgo (%@)" = "%@ sitten";
"tunnelHandshakeTimestampYear (%d)" = "%d vuosi";
"tunnelHandshakeTimestampYears (%d)" = "%d vuotta";
"tunnelHandshakeTimestampDay (%d)" = "%d päivä";
"tunnelHandshakeTimestampDays (%d)" = "%d päivää";
"tunnelHandshakeTimestampHour (%d)" = "%d tunti";
"tunnelHandshakeTimestampHours (%d)" = "%d tuntia";
"tunnelHandshakeTimestampMinute (%d)" = "%d minuutti";
"tunnelHandshakeTimestampMinutes (%d)" = "%d minuuttia";
"tunnelHandshakeTimestampSecond (%d)" = "%d sekunti";
"tunnelHandshakeTimestampSeconds (%d)" = "%d sekuntia";
"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ tuntia";
"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minuuttia";
"tunnelPeerPresharedKeyEnabled" = "käytössä";
// Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */
"alertInvalidInterfaceTitle" = "Virheellinen liittymä";
/* Any one of the following alert messages can go with the above title */
"alertInvalidInterfaceMessageNameRequired" = "Sovittimen nimi vaadittu";
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Sovittimen yksityinen avain on vaadittu";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Sovittimen yksityinen avain on oltava 32-tavuinen avain base64 enkoodauksella";
"alertInvalidInterfaceMessageAddressInvalid" = "Sovittimen osoitteet on oltava pilkulla erotettujen IP-osoitteiden luettelo, valinnaisesti CIDR-notaatiossa";
"alertInvalidInterfaceMessageListenPortInvalid" = "Sovittimen kuunteluportin on tulee olla väliltä 0 - 65535, tai sen on oltava määrittelemätön";
"alertInvalidInterfaceMessageMTUInvalid" = "Sovittimen MTU on oltava väliltä 576 - 65535, tai sen on oltava määrittelemätön";
"alertInvalidInterfaceMessageDNSInvalid" = "Sovittimen DNS-palvelimien on oltava lista pilkulla erotettu IP-osoitteita";
/* Alert title for error in the peer data */
"alertInvalidPeerTitle" = "Virheellinen osapuoli";
/* Any one of the following alert messages can go with the above title */
"alertInvalidPeerMessagePublicKeyRequired" = "Osapuolen julkinen avain on vaadittu";
"alertInvalidPeerMessagePublicKeyInvalid" = "Osapuolen julkinen avain on oltava 32-tavuinen avain base64 -enkoodauksella";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Osapuolen jaettu avain on oltava 32-tavuinen avain base64 -enkoodauksella";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Toisen osapuolen sallittujen IP-osoitteiden on oltava pilkulla erotettujen IP-osoitteiden luettelo, valinnaisesti CIDR-notaatiossa";
"alertInvalidPeerMessageEndpointInvalid" = "Käyttäjän päätepisteen on oltava muotoa ”host:port” tai ”[host]:port”";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Käyttäjän pysyvän keepalivin on oltava välillä 065535 tai määrittelemätön";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Kahdella tai useammalla osapuolella ei voi olla samaa julkista avainta";
// Scanning QR code UI
"scanQRCodeViewTitle" = "Skannaa QR-koodi";
"scanQRCodeTipText" = "Vihje: Luo käyttämällä `qrencode -t ansiutf8 < tunnel.conf`";
// Scanning QR code alerts
"alertScanQRCodeCameraUnsupportedTitle" = "Kameraa ei tuettu";
"alertScanQRCodeCameraUnsupportedMessage" = "Tämä laite ei pysty skannaamaan QR-koodeja";
"alertScanQRCodeInvalidQRCodeTitle" = "Virheellinen QR-koodi";
"alertScanQRCodeUnreadableQRCodeTitle" = "Virheellinen koodi";
// Settings UI
"settingsViewTitle" = "Asetukset";
"settingsSectionTitleAbout" = "Tietoa";
"settingsVersionKeyWireGuardForIOS" = "WireGuard iOS:lle";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go -moottori";
"settingsSectionTitleTunnelLog" = "Loki";
"settingsViewLogButtonTitle" = "Näytä loki";
// Log view
"logViewTitle" = "Loki";
"alertBadArchiveMessage" = "Huono tai korruptoitunut zip arkisto.";
"alertNoTunnelsToExportTitle" = "Ei mitään vietävää";
"alertNoTunnelsToExportMessage" = "Vietäviä tunneleita ei ole";
"alertNoTunnelsInImportedZipArchiveTitle" = "Zip arkistossa ei ole tunneleita";
// Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Aktivointi epäonnistui";
"alertTunnelNameEmptyTitle" = "Nimeä ei annettu lainkaan";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Nimi on jo käytössä";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Aktivointi käynnissä";
// Tunnel management error alerts on system error
/* The alert message that goes with the following titles would be
one of the alertSystemErrorMessage* listed further down */
"alertSystemErrorOnListingTunnelsTitle" = "Tunneleita ei voitu listata";
"alertSystemErrorOnAddTunnelTitle" = "Tunnelia ei voitu luoda";
/* The alert message for this alert shall include
one of the alertSystemErrorMessage* listed further down */
"alertTunnelActivationSystemErrorTitle" = "Aktivointi epäonnistui";
/* alertSystemErrorMessage* messages */
"alertSystemErrorMessageTunnelConfigurationInvalid" = "Konfiguraatio ei kelpaa.";
"alertSystemErrorMessageTunnelConnectionFailed" = "Yhteys epäonnistui.";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Konfiguraation lukeminen tai kirjoittaminen epäonnistui.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Tuntematon järjestelmävirhe.";
// Mac status bar menu / pulldown menu / main menu
"macMenuNetworks (%@)" = "Verkot: %@";
"macMenuNetworksNone" = "Verkot: Ei mitään";
"macMenuTitle" = "WireGuard";
"macMenuManageTunnels" = "Hallitse tunneleita";
"macMenuImportTunnels" = "Tuo tunneli(t) tiedostosta…";
"macMenuAddEmptyTunnel" = "Lisää tyhjä tunneli…";
"macMenuViewLog" = "Näytä loki";
"macMenuExportTunnels" = "Vie tunnelit Zippinä…";
"macMenuAbout" = "Tietoa WireGuardista";
"macMenuQuit" = "Sulje WireGuard";
"macMenuHideApp" = "Piilota WireGuard";
"macMenuHideOtherApps" = "Piilota muut";
"macMenuShowAllApps" = "Näytä kaikki";
"macMenuFile" = "Tiedosto";
"macMenuCloseWindow" = "Sulje ikkuna";
"macMenuEdit" = "Muokkaa";
"macMenuCut" = "Leikkaa";
"macMenuCopy" = "Kopioi";
"macMenuPaste" = "Liitä";
"macMenuSelectAll" = "Valitse kaikki";
"macMenuTunnel" = "Tunneli";
"macMenuToggleStatus" = "Vaihda tilaa";
"macMenuEditTunnel" = "Muokkaa…";
"macMenuDeleteSelected" = "Poista valitut";
"macMenuWindow" = "Ikkuna";
"macMenuMinimize" = "Pienennä";
"macMenuZoom" = "Lähennä/Loitonna";
// Mac manage tunnels window
"macWindowTitleManageTunnels" = "Hallitse WireGuard tunneleita";
"macDeleteTunnelConfirmationAlertMessage (%@)" = "Oletko varma, että haluat poistaa \"%@\"?";
"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Haluatko varmasti poistaa %d kohdetta?";
"macDeleteTunnelConfirmationAlertInfo" = "Tätä toimintoa ei voi peruuttaa.";
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Poista";
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Peruuta";
"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Poistetaan…";
"macButtonImportTunnels" = "Tuo tunneli(t) tiedostosta";
"macSheetButtonImport" = "Tuo";
"macNameFieldExportLog" = "Tallenna loki kohteeseen:";
"macSheetButtonExportLog" = "Tallenna";
"macNameFieldExportZip" = "Vie tunnelit kohteeseen:";
"macSheetButtonExportZip" = "Tallenna";
"macButtonDeleteTunnels (%d)" = "Poista %d tunnelia";
"macButtonEdit" = "Muokkaa";
"macFieldOnDemand" = "Tarvittaessa:";
"macFieldOnDemandSSIDs" = "SSIDt:";
// Mac status display
"macStatus (%@)" = "Tila: %@";
// Mac editing config
"macEditDiscard" = "Hylkää";
"macEditSave" = "Tallenna";
"macAlertNameIsEmpty" = "Nimi on pakollinen";
"macAlertPrivateKeyInvalid" = "Yksityinen avain ei kelpaa.";
"macAlertListenPortInvalid (%@)" = "Portti \"%@\" ei kelpaa.";
"macAlertAddressInvalid (%@)" = "Osoite \"%@\" ei kelpaa.";
"macAlertDNSInvalid (%@)" = "DNS %@ ei kelpaa.";
"macAlertMTUInvalid (%@)" = "MTU %@ ei kelpaa.";
"macAlertPublicKeyInvalid" = "Julkinen avain ei kelpaa";
"macAlertPreSharedKeyInvalid" = "Jaettu avain ei kelpaa";
"macAlertAllowedIPInvalid (%@)" = "Sallitut IP-osoitteet %@\" eivät kelpaa";
"macAlertEndpointInvalid (%@)" = "Päätepiste \"%@\" ei kelpaa";
"macAlertPersistentKeepliveInvalid (%@)" = "Pysyvä keepalive arvo %@ ei kelpaa";
"macAlertUnrecognizedPeerKey (%@)" = "Osapuoli sisältää tunnistamattoman avaimen ”%@”";
"macAlertInfoUnrecognizedPeerKey" = "Voimassa olevat avaimet ovat: ”PublicKey”, ”PresharedKey”, ”AllowedIPs”, ”Endpoint” ja ”PersistentKeepalive”";
// Mac about dialog
"macAppVersion (%@)" = "Sovelluksen versio: %@";
"macGoBackendVersion (%@)" = "Go -moottorin versio: %@";
"iosViewPrivateData" = "Todenna nähdäksesi tunnelin yksityiset avaimet.";
"macConfirmAndQuitAlertQuitWireGuard" = "Sulje WireGuard";
"macConfirmAndQuitAlertCloseWindow" = "Sulje tunneleiden hallinta";
// Mac tooltip
"macToolTipEditTunnel" = "Muokkaa tunnelia (⌘E)";
// Mac log view
"macLogColumnTitleTime" = "Aika";
"macLogColumnTitleLogMessage" = "Lokiviesti";
"macLogButtonTitleClose" = "Sulje";
"macLogButtonTitleSave" = "Tallenna…";
"macUnusableTunnelButtonTitleDeleteTunnel" = "Poista tunneli";
// Mac App Store updating alert
"macAppStoreUpdatingAlertMessage" = "App Store haluaa päivittää WireGuardin";
// Donation
"donateLink" = "♥ Lahjoita WireGuard projektille";
"macAlertNoInterface" = "Configuration must have an Interface section.";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: PrivateKey, ListenPort, Address, DNS and MTU.";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
"settingsExportZipButtonTitle" = "Export zip archive";
"macViewPrivateData" = "view tunnel private keys";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
"alertUnableToWriteLogTitle" = "Log export failed";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
"macAlertMultipleInterfaces" = "Configuration must have only one Interface section.";
"macUnusableTunnelMessage" = "The configuration for this tunnel cannot be found in the keychain.";
"alertBadArchiveTitle" = "Unable to read zip archive";
"macExportPrivateData" = "export tunnel private keys";
"macToolTipToggleStatus" = "Toggle status (⌘T)";
"macTunnelsMenuTitle" = "Tunnels";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";

View File

@ -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
@ -255,8 +255,6 @@
"alertTunnelActivationFileDescriptorFailureMessage" = "Impossible de déterminer le descripteur de fichier TUN.";
"alertTunnelActivationSetNetworkSettingsMessage" = "Impossible d'appliquer les paramètres réseau à l'objet tunnel.";
"alertTunnelActivationFailureOnDemandAddendum" = " Ce tunnel a été activé à la demande, donc ce tunnel pourrait être réactivé automatiquement par l'OS. Vous pouvez désactiver l'activation à la demande dans cette application en modifiant la configuration du tunnel.";
"alertTunnelDNSFailureTitle" = "Échec de la résolution DNS";
"alertTunnelDNSFailureMessage" = "Un ou plusieurs domaines de terminaison n'ont pas pu être résolus.";
@ -443,3 +441,4 @@
// Donation
"donateLink" = "♥ Faire un don au projet WireGuard";
"macTunnelsMenuTitle" = "Tunnels";

Some files were not shown because too many files have changed in this diff Show More