Compare commits

..

249 Commits

Author SHA1 Message Date
Andrej Mihajlov b7f344968d Kit: make it possible to opt-out from automatically raising the reasserting flag.
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2022-02-21 15:54:49 +01:00
Andrej Mihajlov 399013415d Merge branch 'am/ignore-set-network-settings-error-when-offline' into am/develop 2022-02-21 13:11:49 +01:00
Andrej Mihajlov 7dea71898d Kit: Adapter: ignore error when updating tunnel settings in offline
Since around iOS 15.1, calling `setNetworkSettings()` when in
airplane mode, throws an unknown error:

Error Domain=NEAgentErrorDomain Code=1

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2021-12-20 16:21:34 +01:00
Roopesh Chander 23618f994f UI: When saving on-demand rules, deactivate if reqd and then save
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-28 00:16:35 +05:30
Roopesh Chander ba644415c7 UI: When saving on-demand rules on a config, enable on-demand if active
When a user saves on-demand rules on the configuration, set
onDemandEnabled to true if the tunnel is active, and false if it isn't.
Then deactivate the tunnel.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-24 01:01:10 +05:30
Jason A. Donenfeld 10da5cfdef App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-23 06:20:28 +02:00
Jason A. Donenfeld 75b6925deb UI: macOS: increase login detector file timeout
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-23 06:19:48 +02:00
Jason A. Donenfeld 03a59ff38e Model: migrate iOS 14 keychain references to iOS 15 format
Keychain references used to be bijective, but with the change in format,
Apple tried to be too clever, and references are no longer bijective.
This lead to us deleting keychain entries, which in turn emptied out
people's configs upon upgrading to iOS 15. Disaster!

Fix this by detecting the change in format and saving the new password
reference. We still rely on this being bijective moving forward;
hopefully this bug won't repeat itself. It would be nice to not rely on
that property, but doing so without grinding startup to a halt isn't
obviously done, given how slow the keychain accesses are and how limited
the API is.

Reported-by: Eddie <stunnel@attglobal.net>
Reported-by: Anatoli <me@anatoli.ws>
Reported-by: Alan Graham <alan@meshify.app>
Reported-by: Jacob Wilder <oss@jacobwilder.org>
Reported-by: Miguel Arroz <miguel.arroz@gmail.com>
Reported-by: Reid Rankin <reidrankin@gmail.com>
Reported-by: Fabien <patate.cosmique@pm.me>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-23 06:11:19 +02:00
Jason A. Donenfeld abf506c1fe UI: iOS: remove list pinking when no config
This reverts commit 86afd1a46a.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-23 06:08:53 +02:00
Jason A. Donenfeld dfb685f258 WireGuardApp: restore old keychain consistency behavior
This reverts commit adcbd17ebe.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-23 05:40:10 +02:00
Jason A. Donenfeld f3798d0e11 App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 20:59:19 +02:00
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Jason A. Donenfeld d882a486a9 Keychain: remove class constraint when copying
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 16:51:25 +02:00
Jason A. Donenfeld 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
Jason A. Donenfeld 3d8de22b96 WireGuardKitGo: bump wireguard-go version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-09-22 06:58:14 +02:00
Roopesh Chander 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
Roopesh Chander f5a14b8434 MacAppStoreUpdateDetector: Add pid to the log
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
Jason A. Donenfeld 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
Roopesh Chander a8226b35d2 build: Fix swiftlint warnings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-09-22 06:58:14 +02:00
Roopesh Chander 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
Roopesh Chander 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 2c2c53b1f8 UI: macOS: Add yellow circle image
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-30 13:29:09 +05:30
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 03ef79c0fd UI: When reloading tunnels, preserve '.waiting' state
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:35:05 +05:30
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Andrej Mihajlov 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
Roopesh Chander 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
Roopesh Chander df9934a4b8 UI: TunnelsManager: Add setOnDemandEnabled() instance method
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:01 +05:30
Roopesh Chander 40f18de4d2 UI: TunnelsManager: Add TunnelContainer.hasOnDemandRules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2021-07-28 03:18:01 +05:30
Jason A. Donenfeld 13b720442d Global: bump copyright year
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-17 16:56:46 +02:00
Andrej Mihajlov c1f509d65b Kit: add missing import for WireGuardKitC
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2021-06-17 15:15:41 +02:00
Jason A. Donenfeld 87f0526f09 App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 18:34:54 +02:00
Jason A. Donenfeld 060c027325 Kit: Go: mod bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-06-16 18:03:28 +02:00
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Andrej Mihajlov 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Jason A. Donenfeld b9ff5c2e94 README: account for funky xcode paths
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:07 -07:00
Jason A. Donenfeld b7f69d20b6 Kit: Go: bump to latest API
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2021-03-09 09:10:04 -07:00
Andrej Mihajlov 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
Jason A. Donenfeld 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
Jason A. Donenfeld 695f868b1f Kit: Go: mod bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 22:54:47 +01:00
Andrej Mihajlov e724c043d9 UI: iOS: Remove duplicate call to addSubview
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-23 16:14:03 +01:00
Andrej Mihajlov 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
Jason A. Donenfeld c4f79beb8d App: version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-23 15:05:56 +01:00
Jason A. Donenfeld 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
Andrej Mihajlov 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
Andrej Mihajlov 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Andrej Mihajlov 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
Andrej Mihajlov 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
Andrej Mihajlov 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Andrej Mihajlov 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
Jason A. Donenfeld 403ee63615 project: generate more stable locale IDs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 12:41:23 +01:00
Andrej Mihajlov 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
Jason A. Donenfeld 386fe4eb12 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-17 00:43:38 +01:00
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Jason A. Donenfeld 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
Jason A. Donenfeld 4ded3f6bfe UI: macOS: remove donation link
Apple forbids us from having a simple donation link in the "About
WireGuard" dialog, due to new policies. And arguing with the giant is
not going to be a fruitful battle. Do the practical thing and just
remove it.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-16 00:04:32 +01:00
Jason A. Donenfeld b51113f680 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-15 16:18:07 +01:00
Andrej Mihajlov be96dea04a WireGuardApp: Refactor TunnelListCell
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov a786f5df60 WireGuardApp: Replace AnyObject with a concrete NSKeyValueObservation
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov 9a483a46fa WireGuardApp: Animate switch control in TunnelListCell
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov 5d2a337332 WireGuardApp: Remove 200ms delay when updating tunnel status switch
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov facf776602 WireGuardApp: Pin status switch to cell margin
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov d3400e3a80 WireGuardApp: Refactor indicator view initialization
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov 92517bd21e WireGuardApp: Use Bundle.forInfoDictionaryKey to access Info.plist fields
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov 761f635e16 WireGuardApp: Refactor indicator initialization
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Andrej Mihajlov 44704ba892 WireGuardApp: Fix window background color to default black
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-15 15:56:22 +01:00
Jason A. Donenfeld 9231c03513 global: support DNS search domains
This has been supported by Windows and Linux for quite some time. Add
support here for iOS and macOS.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-15 15:54:12 +01:00
Jason A. Donenfeld 27b32e60b2 WireGuardKitGo: update to latest wireguard-go tag
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-15 13:16:52 +01:00
Jason A. Donenfeld 9d5b376dcf Revert "[REVERT ME SOON] TunnelsManager: Workaround for macOS Catalina deleting tunnels arbitrarily"
This reverts commit 028e76eb3f.

It's been over a year. I really hope this is fixed by Apple.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-11 12:51:16 +01:00
Jason A. Donenfeld 8fd4883d7e WireGuardApp: modify xcodeproj when syncing translations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-11 12:39:43 +01:00
Andrej Mihajlov d414cec9aa WireGuardKit: Let wireguard-go backend run in offline on macOS
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-11 11:56:05 +01:00
Jason A. Donenfeld 54e3333b72 WireGuardApp: add CrowdIn syncer and run it
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-11 11:34:19 +01:00
Andrej Mihajlov 9f8d0e24df WireGuardKit: Conditionally turn on/off wireguard-go
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-11 11:15:22 +01:00
Jason A. Donenfeld 3de7c99301 WireGuardGoKit: drop support for armv7
Apple and Go have both dropped it, so we do the same.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-09 15:56:35 +01:00
Jason A. Donenfeld d4fd17cd8f global: fix remaining swiftlint violations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-09 14:35:21 +01:00
Jason A. Donenfeld d875266db5 WireGuardKitGo: get rid of missing -Wno-unused-command-line-argument flag
Recent toolchains error out on it, and it's no longer needed.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-09 14:25:22 +01:00
Jason A. Donenfeld d696e31b6e WireGuardKitGo: rebase boottime patch onto Go 1.15.6
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-09 14:08:45 +01:00
Jason A. Donenfeld 90acf2b220 global: bump year in header
A bit overdue, but better late than never.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-12-04 12:15:29 +01:00
Andrej Mihajlov 27ef0c6dba WireGuardApp: Update target membership to exclude sources that are only used in network extensions
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 11:50:44 +01:00
Andrej Mihajlov 8f67435d4a WireGuardKit: Delegate IPv*Address initialization to self.init
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 11:50:44 +01:00
Andrej Mihajlov b4ebe2440f WireGuardApp: Remove backend version call in Logger.swift & extract wireguard-go version script from network extension targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 11:50:44 +01:00
Andrej Mihajlov d440a91b0e WireGuardKit: Log XLAT resolution errors
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 11:50:43 +01:00
Andrej Mihajlov 9e909a3294 WireGuardApp: Disable SWIFT_PRECOMPILE_BRIDGING_HEADER
Clang automatically picks up module.modulemap files from WireGuardKit directories when precompiling bridging header file, which causes the compiler to fail with obscure error.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 75bcf97ab2 WireGuardApp: Update swift version from 4.2 to 5.0
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 0edde8b46f Update checkout path in README
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov bcc34e0bb6 Keychain: Avoid roundtrip via items when accessing item label (stored in kSecAttrLabel)
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 90b41aed89 Keychain: Remove unnecessary cast to String in Keychain queries
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 7930b94981 WireGuardApp: Remove WireGuardKit.swift from Xcode source tree
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 54a89f6a0e WireGuadKit: Rename WireGuardAdapter.version -> .backendVersion & remove var wireGuardVersion with WireGuardKit.swift
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:09 +01:00
Andrej Mihajlov 8976a53b05 WireGuardApp: Add back the wireguard-go version extraction script and use WIREGUARD_GO_VERSION directly
Avoids linking against libwg-go.a in order to access the WireGuard backend version.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:39:05 +01:00
Andrej Mihajlov 9849dedf1d WireGuardApp: Include headers from WireGuardKitC
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-04 09:38:31 +01:00
Andrej Mihajlov 547077a808 WireGuardApp: integrate WireGuardKit sources directly
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 0b0898dc3c Remove Sources/ in project folder names
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 9f9d1ffed8 WireGuardKit: Rename WireGuardKitSwift -> WireGuardKit
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 5a044e4129 Linter: Fix all linter issues across the codebase
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 35f0ada8a9 WireGuardApp: Fix build working dir for go-bridge targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 39e7ee07ac WireGuardNetworkExtension: Remove wireguard.h from bridging header
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 5a1c9598f1 Fix paths pointing to xcconfigs
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov 101a45b6f1 WireGuardKit: Add wireguard-go files to exclude list to eliminate SwiftPM warnings
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:25 +01:00
Andrej Mihajlov fd527f73e6 WireGuardKit: Set publicHeadersPath = "." to flatten public headers structure
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:24 +01:00
Andrej Mihajlov de6aa3eb58 WireGuardKit: Fix module map for WireGuardKitC
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:24 +01:00
Andrej Mihajlov c9eafd82ac WireGuardKit: Fix import statements
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:24 +01:00
Andrej Mihajlov ec57408570 Move all source files to Sources/ and rename WireGuardKit targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:32:24 +01:00
Andrej Mihajlov 9c38a1b897 WireGuardKit: Assert that resolutionResults must not contain failures
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov b34625f511 WireGuardKit: Only assign self.settingsGenerator upon success to set tunnel network settings to avoid inconsistent state
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 2e356d3d8f WireGuardKit: Remove handleLogLine from WireGuardAdapter
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 697d449dc8 WireGuardKit: Remove isStarted: bool from WireGuardAdapter
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 4f9f61f7a7 WireGuardKit: Fix docs for WireGuardAdapterError
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov bceb0a827d WireGuardKit: Fix docs for WireGuardLogLevel
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 2329f712cf WireGuardKit: Pass logHandler via constructor
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov d2c38702c8 Packet tunnel: Remove last error in the completion handler given to adapter.stop
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov def921801f WireGuardKit: Rename cannotLocateSocketDescriptor -> cannotLocateTunnelFileDescriptor in WireGuardAdapterError
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 41e006a407 WireGuardApp: Switch WireGuardKit to master branch
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 384b514290 WireGuardKit: Add TODO to log the error coming from withReresolvedIP
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov a6858bd126 WireGuardKit: Change getWireGuardVersion() -> wireGuardVersion
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov ef7de2500f Update README
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-03 13:22:52 +01:00
Andrej Mihajlov 6099975b71 Packet tunnel: Implement packet tunnel provider using WireGuardAdapter
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:09 +01:00
Andrej Mihajlov 828756e8ba WireGuardKit: Add WireGuardAdapter
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:09 +01:00
Andrej Mihajlov 4deaf905c1 WireGuardKit: Add wrappers for PrivateKey, PublicKey, PreSharedKey
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:09 +01:00
Andrej Mihajlov 76c8487a56 iOS/macOS: Remove "Extract wireguard-go version" build phase
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:09 +01:00
Andrej Mihajlov a05f1233f9 iOS/macOS: Remove main bundle apps dependence on WireGuardgoBridge.
Main bundle apps do not have to depend on WireGuardGoBridge<PLATFORM> as they depend on network extnesions which in turn depend on WireGuardGoBridge<PLATFORM>.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Andrej Mihajlov 95b833c754 iOS/macOS: Integrate WireGuardKit
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Jason A. Donenfeld 8c057bf928 go-bridge: Add context support for wgSetLogger
Cherry picked cda99bf45c3cb95ca56204549689a0ae91ff4813 from jd/loggerCtx with the fix for wgSetLogger signature in the C header file.

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Andrej Mihajlov ddf8ade9c6 WireGuardKit: Add WireGuardKitCTarget with private C sources
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Andrej Mihajlov 4cb21b5eb0 WireGuardKit: Set public access level for shared structs
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Andrej Mihajlov a03df7d8cc WireGuardKit: Move shared structs to WireGuardKit
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Andrej Mihajlov 57f66f16f8 WireGuardKit: Add swift package scaffolding
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
2020-12-02 11:08:08 +01:00
Jason A. Donenfeld 737f847c0d go-bridge: dup tunFd so as to not confuse NetworkExtension
The extension isn't banking on tunFd being closed ever, so dup it before
handing it to the rest of wireguard-go.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-09-02 17:21:37 +02:00
Andrej Mihajlov 671a594945 Change QoS to .utility
As per comment by eskimo:
https://developer.apple.com/forums/thread/107904?answerId=328525022#328525022

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-25 17:50:15 -06:00
Andrej Mihajlov 3646430528 Make sure that the tunnel and path monitor run on the same serial queue
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-25 17:50:15 -06:00
Andrej Mihajlov e9bd6e576f Fix retain cycle between NWPathMonitor and PacketTunnelProvider
See: https://www.marisibrothers.com/2017/04/memory-leak-in-swift-assigning-function.html

Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-25 17:50:15 -06:00
Andrej Mihajlov 35300d1c5f Refactor interface name query
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-06-25 17:50:03 -06:00
Jason A. Donenfeld 112545248e Localization: Update Japanese
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-11 23:28:05 -06:00
Roopesh Chander 78e6ecc4bc Localization: macOS: Add translations for 'Edit' button
By copying the 'macMenuEdit' entries to 'macButtonEdit'.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2020-04-11 16:57:51 +05:30
Roopesh Chander 174a6e8e32 Localization: macOS: Fix localization for 'Edit' button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2020-04-11 16:56:54 +05:30
Roopesh Chander 20bcabbca4 Localization: Add German translation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2020-04-11 16:41:45 +05:30
Roopesh Chander 0a3554cedd Localization: Add Italian translation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2020-04-11 16:41:45 +05:30
Roopesh Chander 2acc7db63d Localization: Wire up Japanese translation
By adding the translated Localizable.strings to the Xcode project

Signed-off-by: Roopesh Chander <roop@roopc.net>
2020-04-11 16:41:45 +05:30
Jason A. Donenfeld 31af7049fc highlighter: insist on 256-bit keys, not 257-bit or 258-bit
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-04-03 23:17:56 -06:00
Eiji Tanioka 52062a45c1 Japanese Translation
Translation for wireguard-apple. Checked on Xcode iOS simulator but not
all messages.

Signed-off-by: Eiji Tanioka <tanioka404@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-23 10:02:49 +01:00
Andrej Mihajlov 30406dec6d wireguard-go-bridge: use C string instead of gostring_t
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-02-07 22:35:57 +01:00
Jason A. Donenfeld edde27a0a0 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-27 12:10:53 +01:00
Jason A. Donenfeld cfff596c30 wireguard-go-bridge: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2020-01-27 12:10:21 +01:00
Jason A. Donenfeld ba1c968cdf Update repo urls
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-12-30 11:54:13 +01:00
Jason A. Donenfeld c48406ac38 wireguard-go-bridge: style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-20 09:59:21 +01:00
Jason A. Donenfeld 14437477e6 README: specify required version in readme
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-05 18:59:24 +08:00
Jason A. Donenfeld 68d928192b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-05 17:25:24 +08:00
Roopesh Chander 028e76eb3f [REVERT ME SOON] TunnelsManager: Workaround for macOS Catalina deleting tunnels arbitrarily
In macOS Catalina, for some users, the tunnels get deleted arbitrarily
by the OS. It's not clear what triggers that.

As a workaround, in macOS Catalina, when we realize that tunnels have
been deleted outside the app, we reinstate those tunnels using the
information in the keychain.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-11-05 17:25:21 +08:00
Jason A. Donenfeld cb0c965294 wireguard-go-bridge: update to 1.13.4
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-11-05 17:20:31 +08:00
Jason A. Donenfeld d7ce621cb2 UI: iOS: more dark mode fixes
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-25 10:59:16 +02:00
Jason A. Donenfeld a1ca4f6eb5 wireguard-go-bridge: work around Go 1.13.3 regression
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-25 10:36:58 +02:00
Jason A. Donenfeld 437f0dc46d Revert "NetworkExtension: don't use exit(0) hack on Catalina"
This reverts commit 3619279a65d9a506fb13d7f24909b38a5202fa8f.

Still broken!

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-15 16:51:50 +02:00
Jason A. Donenfeld 547eabb4ae Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-15 14:53:34 +02:00
Jason A. Donenfeld bb16d3ebc8 iOS: UI: Make edit views full screen modal
This might be worse on the iPad. Oh well.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-15 14:51:04 +02:00
Jason A. Donenfeld 1b6170cbc9 NetworkExtension: don't use exit(0) hack on Catalina
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-15 11:44:13 +02:00
Diab Neiroukh 4c37a4b7a7 UI: iOS: adjust colors for iOS 13
To be compatible with Dark Mode, we need to change some of our
color references to be "dynamic".

Signed-off-by: Diab Neiroukh <officiallazerl0rd@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-15 00:01:17 +02:00
Jason A. Donenfeld 226166bdaf Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 22:20:37 +02:00
Jason A. Donenfeld 80fa72cabc iOS: UI: abort is optimized out in release builds
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-12 22:20:37 +02:00
Jason A. Donenfeld d976d159d0 Keychain: make verification errors only happen when we're sure it's due to not found
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-11 22:07:18 +02:00
Jason A. Donenfeld 84ca7fcf40 ui: add donation link
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-11 21:44:12 +02:00
Jason A. Donenfeld f120a6aab0 wireguard-go-bridge: reduce version checks and cleanup
We now rely on -trimpath which restricts us to >= 1.13, and the patch
application should fail too. This has the downside that the user will
need to clean their xcode project when they upgrade go, though.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-08 16:59:02 +02:00
Jason A. Donenfeld 0d8108d8da wireguard-go-bridge: update for 1.13
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-10-04 18:28:53 +02:00
Jason A. Donenfeld e072ebee58 UI: iOS: set CFBundleDisplayName to satisfy new ITMS-90783 error
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-28 14:42:43 +02:00
Jason A. Donenfeld c6767d9007 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-28 13:51:32 +02:00
Jason A. Donenfeld bb5760cca4 WgQuickConfig: Swift treats \r\n as a single character
let blah = "hello\nworld\ndoes\nthis\nwork"
print(blah.split(separator: "\n"))
//output: ["hello", "world", "does", "this", "work"]

let blah2 = "hello\r\nworld\r\ndoes\r\nthis\r\nwork"
print(blah2.split(separator: "\n"))
//output: ["hello\r\nworld\r\ndoes\r\nthis\r\nwork"]
//expected: ["hello\r", "world\r", "does\r", "this\r", "work\r"]

In blah2, the string splitting fails because swift considers \r\n to be
its own character.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-28 12:26:39 +02:00
Jason A. Donenfeld 26b7971ba6 UI: macOS: Show useful error message on .conf import
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-28 12:07:18 +02:00
Roopesh Chander b286ede3c6 iOS: Importing: If tunnelsManager isn't ready yet, we should wait for it
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-13 23:02:54 +05:30
Roopesh Chander 4ef7afe3ca macOS: Tunnel detail: Handle deletion outside app, again
This was previously done in commit f281b93, but the changes in commit
1507a97 for handling deletion of multiple tunnels undid this capability.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-13 22:21:31 +05:30
Jason A. Donenfeld 377f2f0496 TunnelsManager: store UID on macOS for keychain availability
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-11 02:18:42 +02:00
Jason A. Donenfeld 7ed5893fc6 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-10 18:58:18 +02:00
Jason A. Donenfeld e70c397e54 TunnelProvider: remove all cleverness
This will cause more socket flaps than necessary but hopefully will fix
some bugs.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-10 18:47:39 +02:00
Roopesh Chander 6a6be9edde on-demand: iOS: Fix crash on selecting Any SSID when already selected
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-09 23:55:44 +05:30
Roopesh Chander 37f8500fe6 on-demand: Don't crash on encountering unexpected on-demand rules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-09 23:55:39 +05:30
Roopesh Chander 207f82dd9d macOS: Remove unused strings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-09 11:39:06 +02:00
Jason A. Donenfeld de8fedf87a Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-09 11:39:06 +02:00
Jason A. Donenfeld 98d306da5b macOS: remove store update escape hatch
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-09 11:39:06 +02:00
Jason A. Donenfeld c7b7b1247b TunnelProvider: store the entire NWPath
Otherwise [utun0, en0] == [en0, utun0] before WiFi has connected, and we
wind up not rebinding after WiFi does successfully connect, which means
people have trouble when resuming from sleep.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-09 11:39:06 +02:00
Jason A. Donenfeld a66f13eb01 README: update repo location
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-09 11:39:06 +02:00
Jason A. Donenfeld f50e7ae686 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-06-06 10:27:39 +02:00
Roopesh Chander 4d6692548c macOS: App menu > Quit shall show a prompt to quit or close window
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-06 10:27:11 +02:00
Roopesh Chander 1dccd39818 macOS: Save/restore the log window's size
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-04 20:34:37 +05:30
Roopesh Chander 4cb775c72f macOS: Log view: Allow resizing horizontally
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-06-04 15:48:42 +05:30
Jason A. Donenfeld 4cb783c447 go-bridge: bump version
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-31 19:20:51 +02:00
Jason A. Donenfeld d20daa345a Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-31 17:30:06 +02:00
Jason A. Donenfeld 168ba2da8a NetworkExtension: bump sockets on path change
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-31 17:29:29 +02:00
Roopesh Chander 714d6a41bd macOS: Dismiss modals correctly
Previously, the presented vc were leaking when discarding edits
or when closing the log view controller.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-31 17:29:29 +02:00
Roopesh Chander 9b92a8f933 macOS: Update app icon
Reduce the size and add a drop shadow

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-31 17:29:29 +02:00
Roopesh Chander 5e9780ef8f iOS: Should be able to re-show tunnel detail
Fixes a bug introduced in the refactoring in
commit 7322fb084087774e8b58e347902f6d7036cbde5c

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-27 15:36:39 +05:30
Roopesh Chander 9faf814e8b macOS: Tunnel detail: No need to update runtime info on tunnelSaved()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-27 14:43:41 +05:30
Roopesh Chander 30da10a0e9 macOS: Start refreshing runtime info in viewWillAppear(), not init()
Because when the window is closed and reopened, we should start
refreshing runtime info again.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-27 14:38:17 +05:30
Roopesh Chander a18614d6b3 macOS: Fix residual menu highlight on reopen
If we close the window with Cmd+W or Cmd+Q and then re-launch the app,
the main menu shows residual highlight from the close action. This
commit fixes that.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-26 00:12:47 +05:30
Jason A. Donenfeld 5100e597aa macOS: do not call out to recent tunnels tracker
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-26 00:12:47 +05:30
Jason A. Donenfeld 0340641c4c NetworkExtension: apparently the extension process is scoped properly anyway
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-26 00:12:47 +05:30
Jason A. Donenfeld 813dea6902 NetworkExtension: use excludedRoutes instead of binding on iOS
The networking stack there is to flaky and the notifier doesn't always
fire correctly. Hopefully excludedRoutes works well with XLAT; otherwise
we're in trouble.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-05-26 00:12:47 +05:30
Roopesh Chander c30d491edc iOS: Should be able to call showTunnelDetail multiple times
And the detail views should not stack up.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-26 00:12:42 +05:30
Roopesh Chander 88c80d6694 iOS: Refactor showing of the tunnel detail
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-25 13:24:01 +02:00
Roopesh Chander 393718dfaf iOS: Show Home screen quick actions for recent tunnels
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-25 13:24:01 +02:00
Roopesh Chander f852b6f919 iOS: Keep track of most-recently-activated tunnels
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-25 13:24:01 +02:00
Roopesh Chander 8926434682 macOS: Workaround for unresponsive main menu when launched from Xcode
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-22 19:52:21 +05:30
Roopesh Chander 493c7b102e macOS: Ignore bogus reopen because of login item helper
The bogus reopen occurs because the SMLoginItemSetEnabled actually runs
the helper app immediately. The helper app attempts to launch the main
app, causing a reopen Apple event (rapp) to be sent.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-22 19:52:21 +05:30
Roopesh Chander 717bc8a26f macOS: Workaround for unresponsive main menu after reopen
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-22 19:52:12 +05:30
Roopesh Chander e582155a10 macOS: Ensure window is shown on app reopening
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-22 15:19:57 +05:30
Roopesh Chander 70d19691a7 macOS: Simplify detecting the type of an Apple event
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:28 +05:30
Roopesh Chander 40b1f0bac8 macOS: Don't show manage window when launched at login
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:28 +05:30
Roopesh Chander 52ac9b82c2 macOS: Login item: Get helper app version from xcconfig
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander fc1fdbbcdb macOS: Login item: Fix Info.plist path
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 300268daa0 macOS: Show Manage Tunnels window on startup
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 9bf304a9ac macOS: Minor refactor of StatusMenuWindowDelegate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 586a592b68 macOS: Disable 'Delete Selected' when nothing is selected
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander c0526d2efb macOS: Some menu item titles are automatically inferred
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander fdbd4f875e macOS: Use title-style capitalization for menu items
As per https://developer.apple.com/design/human-interface-guidelines/macos/menus/menu-anatomy/

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 4a037cc706 macOS: Make it clear that status menu Quit quits the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 5190fc2249 macOS: Quit in main menu shall just close the window
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 3f25d54dcc macOS: Get back removing tunnel using the Delete key
This now works only when the list view has focus

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander f9880907a2 macOS: Both list and detail main menu items should be always enabled
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 404fa741e8 macOS: swiftlint: Suppress incorrect warnings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 6e1f03e41c macOS: Set a main menu for the app
The main menu would be shown only when the manage tunnels window
is visible.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 6d8965e97d macOS: Remove custom key event handling
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 5e5481b69b macOS: Show app in dock when showing the manage tunnels window
This way, the app can participate in Cmd+Tab

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 69b33c0fad macOS: Edit view: Save on Cmd+S
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 15:45:26 +05:30
Roopesh Chander 167e4f0bf2 macOS: Edit view: Dismiss on Esc
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 13:23:52 +05:30
Roopesh Chander 6e3b28852a macOS: Log view: Dismiss on Esc
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 13:21:42 +05:30
Roopesh Chander 5914e868ab iOS: Log view: Improve the look
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-28 14:29:22 +05:30
Jason A. Donenfeld 83ea9d6fa7 wireguard-go-bridge: add missing format specifier for error
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-24 13:24:03 +02:00
260 changed files with 11669 additions and 2857 deletions
+5 -1
View File
@@ -31,6 +31,10 @@ xcuserdata
*.hmap *.hmap
*.ipa *.ipa
# Swift Package Manager
.swiftpm
.build/
# Fastlane # Fastlane
*.app.dSYM.zip *.app.dSYM.zip
*.mobileprovision *.mobileprovision
@@ -41,7 +45,7 @@ Preview.html
output output
# Wireguard specific # Wireguard specific
WireGuard/WireGuard/Config/Developer.xcconfig Sources/WireGuardApp/Config/Developer.xcconfig
# Vim # Vim
.*.sw* .*.sw*
@@ -7,6 +7,7 @@ disabled_rules:
- type_body_length - type_body_length
- function_body_length - function_body_length
- nesting - nesting
- inclusive_language
opt_in_rules: opt_in_rules:
- empty_count - empty_count
- empty_string - empty_string
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
+2 -2
View File
@@ -136,5 +136,5 @@ Here's an example WireGuard configuration payload dictionary:
Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once. Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once.
[wg-quick(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8 [wg-quick(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8 [wg(8)]: https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
+40
View File
@@ -0,0 +1,40 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "WireGuardKit",
platforms: [
.macOS(.v10_14),
.iOS(.v12)
],
products: [
.library(name: "WireGuardKit", targets: ["WireGuardKit"])
],
dependencies: [],
targets: [
.target(
name: "WireGuardKit",
dependencies: ["WireGuardKitGo", "WireGuardKitC"]
),
.target(
name: "WireGuardKitC",
dependencies: [],
publicHeadersPath: "."
),
.target(
name: "WireGuardKitGo",
dependencies: [],
exclude: [
"goruntime-boottime-over-monotonic.diff",
"go.mod",
"go.sum",
"api-apple.go",
"Makefile"
],
publicHeadersPath: ".",
linkerSettings: [.linkedLibrary("wg-go")]
)
]
)
+55 -7
View File
@@ -1,22 +1,24 @@
# [WireGuard](https://www.wireguard.com/) for iOS and macOS # [WireGuard](https://www.wireguard.com/) for iOS and macOS
This project contains an application for iOS and for macOS, as well as many components shared between the two of them. You may toggle between the two platforms by selecting the target from within Xcode.
## Building ## Building
- Clone this repo recursively: - Clone this repo:
``` ```
$ git clone --recursive https://git.zx2c4.com/wireguard-ios $ git clone https://git.zx2c4.com/wireguard-apple
$ cd wireguard-ios $ cd wireguard-apple
``` ```
- Rename and populate developer team ID file: - Rename and populate developer team ID file:
``` ```
$ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/Config/Developer.xcconfig $ cp Sources/WireGuardApp/Config/Developer.xcconfig.template Sources/WireGuardApp/Config/Developer.xcconfig
$ vim WireGuard/WireGuard/Config/Developer.xcconfig $ vim Sources/WireGuardApp/Config/Developer.xcconfig
``` ```
- Install swiftlint and go: - Install swiftlint and go 1.15:
``` ```
$ brew install swiftlint go $ brew install swiftlint go
@@ -25,11 +27,57 @@ $ brew install swiftlint go
- Open project in Xcode: - Open project in Xcode:
``` ```
$ open ./WireGuard/WireGuard.xcodeproj $ open WireGuard.xcodeproj
``` ```
- Flip switches, press buttons, and make whirling noises until Xcode builds it. - Flip switches, press buttons, and make whirling noises until Xcode builds it.
## WireGuardKit integration
1. Open your Xcode project and add the Swift package with the following URL:
```
https://git.zx2c4.com/wireguard-apple
```
2. `WireGuardKit` links against `wireguard-go-bridge` library, but it cannot build it automatically
due to Swift package manager limitations. So it needs a little help from a developer.
Please follow the instructions below to create a build target(s) for `wireguard-go-bridge`.
- In Xcode, click File -> New -> Target. Switch to "Other" tab and choose "External Build
System".
- Type in `WireGuardGoBridge<PLATFORM>` under the "Product name", replacing the `<PLATFORM>`
placeholder with the name of the platform. For example, when targeting macOS use `macOS`, or
when targeting iOS use `iOS`.
Make sure the build tool is set to: `/usr/bin/make` (default).
- In the appeared "Info" tab of a newly created target, type in the "Directory" path under
the "External Build Tool Configuration":
```
${BUILD_DIR%Build/*}SourcePackages/checkouts/wireguard-apple/Sources/WireGuardKitGo
```
- Switch to "Build Settings" and find `SDKROOT`.
Type in `macosx` if you target macOS, or type in `iphoneos` if you target iOS.
3. Go to Xcode project settings and locate your network extension target and switch to
"Build Phases" tab.
- Locate "Dependencies" section and hit "+" to add `WireGuardGoBridge<PLATFORM>` replacing
the `<PLATFORM>` placeholder with the name of platform matching the network extension
deployment target (i.e macOS or iOS).
- Locate the "Link with binary libraries" section and hit "+" to add `WireGuardKit`.
4. In Xcode project settings, locate your main bundle app and switch to "Build Phases" tab.
Locate the "Link with binary libraries" section and hit "+" to add `WireGuardKit`.
5. iOS only: Locate Bitcode settings under your application target, Build settings -> Enable Bitcode,
change the corresponding value to "No".
Note that if you ship your app for both iOS and macOS, make sure to repeat the steps 2-4 twice,
once per platform.
## MIT License ## MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import os.log import os.log
@@ -35,6 +35,10 @@ extension FileManager {
return sharedFolderURL?.appendingPathComponent("last-error.txt") return sharedFolderURL?.appendingPathComponent("last-error.txt")
} }
static var loginHelperTimestampURL: URL? {
return sharedFolderURL?.appendingPathComponent("login-helper-timestamp.bin")
}
static func deleteFile(at url: URL) -> Bool { static func deleteFile(at url: URL) -> Bool {
do { do {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import Security import Security
@@ -7,9 +7,8 @@ import Security
class Keychain { class Keychain {
static func openReference(called ref: Data) -> String? { static func openReference(called ref: Data) -> String? {
var result: CFTypeRef? var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword, let ret = SecItemCopyMatching([kSecValuePersistentRef: ref,
kSecValuePersistentRef as String: ref, kSecReturnData: true] as CFDictionary,
kSecReturnData as String: true] as CFDictionary,
&result) &result)
if ret != errSecSuccess || result == nil { if ret != errSecSuccess || result == nil {
wg_log(.error, message: "Unable to open config from keychain: \(ret)") wg_log(.error, message: "Unable to open config from keychain: \(ret)")
@@ -21,29 +20,30 @@ class Keychain {
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? { static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
var ret: OSStatus var ret: OSStatus
guard var id = Bundle.main.bundleIdentifier else { guard var bundleIdentifier = Bundle.main.bundleIdentifier else {
wg_log(.error, staticMessage: "Unable to determine bundle identifier") wg_log(.error, staticMessage: "Unable to determine bundle identifier")
return nil return nil
} }
if id.hasSuffix(".network-extension") { if bundleIdentifier.hasSuffix(".network-extension") {
id.removeLast(".network-extension".count) bundleIdentifier.removeLast(".network-extension".count)
} }
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword, let itemLabel = "WireGuard Tunnel: \(name)"
kSecAttrLabel as String: "WireGuard Tunnel: " + name, var items: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount as String: name + ": " + UUID().uuidString, kSecAttrLabel: itemLabel,
kSecAttrDescription as String: "wg-quick(8) config", kSecAttrAccount: name + ": " + UUID().uuidString,
kSecAttrService as String: id, kSecAttrDescription: "wg-quick(8) config",
kSecValueData as String: value.data(using: .utf8) as Any, kSecAttrService: bundleIdentifier,
kSecReturnPersistentRef as String: true] kSecValueData: value.data(using: .utf8) as Any,
kSecReturnPersistentRef: true]
#if os(iOS) #if os(iOS)
items[kSecAttrAccessGroup as String] = FileManager.appGroupId items[kSecAttrAccessGroup] = FileManager.appGroupId
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock items[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
#elseif os(macOS) #elseif os(macOS)
items[kSecAttrSynchronizable as String] = false items[kSecAttrSynchronizable] = false
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly items[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else { guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex", isDirectory: true).path else {
wg_log(.error, staticMessage: "Unable to determine app extension path") wg_log(.error, staticMessage: "Unable to determine app extension path")
return nil return nil
} }
@@ -60,14 +60,12 @@ class Keychain {
return nil return nil
} }
var access: SecAccess? var access: SecAccess?
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString, ret = SecAccessCreate(itemLabel as CFString, [extensionApp!, mainApp!] as CFArray, &access)
[extensionApp!, mainApp!] as CFArray,
&access)
if ret != errSecSuccess || access == nil { if ret != errSecSuccess || access == nil {
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)") wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
return nil return nil
} }
items[kSecAttrAccess as String] = access! items[kSecAttrAccess] = access!
#else #else
#error("Unimplemented") #error("Unimplemented")
#endif #endif
@@ -85,7 +83,7 @@ class Keychain {
} }
static func deleteReference(called ref: Data) { static func deleteReference(called ref: Data) {
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary) let ret = SecItemDelete([kSecValuePersistentRef: ref] as CFDictionary)
if ret != errSecSuccess { if ret != errSecSuccess {
wg_log(.error, message: "Unable to delete config from keychain: \(ret)") wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
} }
@@ -93,10 +91,10 @@ class Keychain {
static func deleteReferences(except whitelist: Set<Data>) { static func deleteReferences(except whitelist: Set<Data>) {
var result: CFTypeRef? var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword, let ret = SecItemCopyMatching([kSecClass: kSecClassGenericPassword,
kSecAttrService as String: Bundle.main.bundleIdentifier as Any, kSecAttrService: Bundle.main.bundleIdentifier as Any,
kSecMatchLimit as String: kSecMatchLimitAll, kSecMatchLimit: kSecMatchLimitAll,
kSecReturnPersistentRef as String: true] as CFDictionary, kSecReturnPersistentRef: true] as CFDictionary,
&result) &result)
if ret != errSecSuccess || result == nil { if ret != errSecSuccess || result == nil {
return return
@@ -110,8 +108,7 @@ class Keychain {
} }
static func verifyReference(called ref: Data) -> Bool { static func verifyReference(called ref: Data) -> Bool {
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword, return SecItemCopyMatching([kSecValuePersistentRef: ref] as CFDictionary,
kSecValuePersistentRef as String: ref] as CFDictionary, nil) != errSecItemNotFound
nil) == errSecSuccess
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import os.log import os.log
@@ -49,8 +49,8 @@ public class Logger {
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))" appVersion += " (\(appBuild))"
} }
let goBackendVersion = WIREGUARD_GO_VERSION
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)") Logger.global?.log(message: "App version: \(appVersion)")
} }
} }
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. * Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
*/ */
#include <string.h> #include <string.h>
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: MIT /* SPDX-License-Identifier: MIT
* *
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. * Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
*/ */
#ifndef RINGLOGGER_H #ifndef RINGLOGGER_H
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
@@ -22,6 +22,9 @@ extension NETunnelProviderProtocol {
if passwordReference == nil { if passwordReference == nil {
return nil return nil
} }
#if os(macOS)
providerConfiguration = ["UID": getuid()]
#endif
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint } let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
if endpoints.count == 1 { if endpoints.count == 1 {
@@ -60,11 +63,46 @@ extension NETunnelProviderProtocol {
* in the keychain. But it's still useful to keep the migration * in the keychain. But it's still useful to keep the migration
* around so that .mobileconfig files are easier. * around so that .mobileconfig files are easier.
*/ */
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false } if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
providerConfiguration = nil #if os(macOS)
guard passwordReference == nil else { return true } providerConfiguration = ["UID": getuid()]
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'") #elseif os(iOS)
passwordReference = Keychain.makeReference(containing: oldConfig, called: name) providerConfiguration = nil
return true #else
#error("Unimplemented")
#endif
guard passwordReference == nil else { return true }
wg_log(.info, message: "Migrating tunnel configuration '\(name)'")
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
return true
}
#if os(macOS)
if passwordReference != nil && providerConfiguration?["UID"] == nil && verifyConfigurationReference() {
providerConfiguration = ["UID": getuid()]
return true
}
#elseif os(iOS)
if #available(iOS 15, *) {
/* Update the stored reference from the old iOS 14 one to the canonical iOS 15 one.
* The iOS 14 ones are 96 bits, while the iOS 15 ones are 160 bits. We do this so
* that we can have fast set exclusion in deleteReferences safely. */
if passwordReference != nil && passwordReference!.count == 12 {
var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecValuePersistentRef: passwordReference!,
kSecReturnPersistentRef: true] as CFDictionary,
&result)
if ret != errSecSuccess || result == nil {
return false
}
guard let newReference = result as? Data else { return false }
if !newReference.elementsEqual(passwordReference!) {
wg_log(.info, message: "Migrating iOS 14-style keychain reference to iOS 15-style keychain reference for '\(name)'")
passwordReference = newReference
return true
}
}
}
#endif
return false
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -39,7 +39,7 @@ extension TunnelConfiguration {
var interfaceConfiguration: InterfaceConfiguration? var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]() var peerConfigurations = [PeerConfiguration]()
let lines = wgQuickConfig.split(separator: "\n") let lines = wgQuickConfig.split { $0.isNewline }
var parserState = ParserState.notInASection var parserState = ParserState.notInASection
var attributes = [String: String]() var attributes = [String: String]()
@@ -111,7 +111,7 @@ extension TunnelConfiguration {
} }
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count { if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey throw ParseError.multiplePeersWithSamePublicKey
} }
@@ -125,9 +125,7 @@ extension TunnelConfiguration {
func asWgQuickConfig() -> String { func asWgQuickConfig() -> String {
var output = "[Interface]\n" var output = "[Interface]\n"
if let privateKey = interface.privateKey.base64Key() { output.append("PrivateKey = \(interface.privateKey.base64Key)\n")
output.append("PrivateKey = \(privateKey)\n")
}
if let listenPort = interface.listenPort { if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n") output.append("ListenPort = \(listenPort)\n")
} }
@@ -135,8 +133,10 @@ extension TunnelConfiguration {
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ") let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("Address = \(addressString)\n") output.append("Address = \(addressString)\n")
} }
if !interface.dns.isEmpty { if !interface.dns.isEmpty || !interface.dnsSearch.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ") var dnsLine = interface.dns.map { $0.stringRepresentation }
dnsLine.append(contentsOf: interface.dnsSearch)
let dnsString = dnsLine.joined(separator: ", ")
output.append("DNS = \(dnsString)\n") output.append("DNS = \(dnsString)\n")
} }
if let mtu = interface.mtu { if let mtu = interface.mtu {
@@ -145,10 +145,8 @@ extension TunnelConfiguration {
for peer in peers { for peer in peers {
output.append("\n[Peer]\n") output.append("\n[Peer]\n")
if let publicKey = peer.publicKey.base64Key() { output.append("PublicKey = \(peer.publicKey.base64Key)\n")
output.append("PublicKey = \(publicKey)\n") if let preSharedKey = peer.preSharedKey?.base64Key {
}
if let preSharedKey = peer.preSharedKey?.base64Key() {
output.append("PresharedKey = \(preSharedKey)\n") output.append("PresharedKey = \(preSharedKey)\n")
} }
if !peer.allowedIPs.isEmpty { if !peer.allowedIPs.isEmpty {
@@ -170,7 +168,7 @@ extension TunnelConfiguration {
guard let privateKeyString = attributes["privatekey"] else { guard let privateKeyString = attributes["privatekey"] else {
throw ParseError.interfaceHasNoPrivateKey throw ParseError.interfaceHasNoPrivateKey
} }
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString) throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
} }
var interface = InterfaceConfiguration(privateKey: privateKey) var interface = InterfaceConfiguration(privateKey: privateKey)
@@ -192,13 +190,16 @@ extension TunnelConfiguration {
} }
if let dnsString = attributes["dns"] { if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]() var dnsServers = [DNSServer]()
var dnsSearch = [String]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) { for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let dnsServer = DNSServer(from: dnsServerString) else { if let dnsServer = DNSServer(from: dnsServerString) {
throw ParseError.interfaceHasInvalidDNS(dnsServerString) dnsServers.append(dnsServer)
} else {
dnsSearch.append(dnsServerString)
} }
dnsServers.append(dnsServer)
} }
interface.dns = dnsServers interface.dns = dnsServers
interface.dnsSearch = dnsSearch
} }
if let mtuString = attributes["mtu"] { if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else { guard let mtu = UInt16(mtuString) else {
@@ -213,12 +214,12 @@ extension TunnelConfiguration {
guard let publicKeyString = attributes["publickey"] else { guard let publicKeyString = attributes["publickey"] else {
throw ParseError.peerHasNoPublicKey throw ParseError.peerHasNoPublicKey
} }
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { guard let publicKey = PublicKey(base64Key: publicKeyString) else {
throw ParseError.peerHasInvalidPublicKey(publicKeyString) throw ParseError.peerHasInvalidPublicKey(publicKeyString)
} }
var peer = PeerConfiguration(publicKey: publicKey) var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["presharedkey"] { if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { guard let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) else {
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString) throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
} }
peer.preSharedKey = preSharedKey peer.preSharedKey = preSharedKey
+33
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)
}
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
// iOS permission prompts // iOS permission prompts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
// Generic alert action names // Generic alert action names
@@ -17,6 +17,7 @@
"tunnelsListSelectAllButtonTitle" = "Select All"; "tunnelsListSelectAllButtonTitle" = "Select All";
"tunnelsListDeleteButtonTitle" = "Delete"; "tunnelsListDeleteButtonTitle" = "Delete";
"tunnelsListSelectedTitle (%d)" = "%d selected"; "tunnelsListSelectedTitle (%d)" = "%d selected";
"tunnelListCaptionOnDemand" = "On-Demand";
// Tunnels list menu // Tunnels list menu
@@ -55,6 +56,11 @@
"tunnelStatusRestarting" = "Restarting"; "tunnelStatusRestarting" = "Restarting";
"tunnelStatusWaiting" = "Waiting"; "tunnelStatusWaiting" = "Waiting";
"tunnelStatusAddendumOnDemand" = " (On-Demand)";
"tunnelStatusOnDemandDisabled" = "On-Demand Disabled";
"tunnelStatusAddendumOnDemandEnabled" = ", On-Demand Enabled";
"tunnelStatusAddendumOnDemandDisabled" = ", On-Demand Disabled";
"macToggleStatusButtonActivate" = "Activate"; "macToggleStatusButtonActivate" = "Activate";
"macToggleStatusButtonActivating" = "Activating…"; "macToggleStatusButtonActivating" = "Activating…";
"macToggleStatusButtonDeactivate" = "Deactivate"; "macToggleStatusButtonDeactivate" = "Deactivate";
@@ -62,6 +68,9 @@
"macToggleStatusButtonReasserting" = "Reactivating…"; "macToggleStatusButtonReasserting" = "Reactivating…";
"macToggleStatusButtonRestarting" = "Restarting…"; "macToggleStatusButtonRestarting" = "Restarting…";
"macToggleStatusButtonWaiting" = "Waiting…"; "macToggleStatusButtonWaiting" = "Waiting…";
"macToggleStatusButtonEnableOnDemand" = "Enable On-Demand";
"macToggleStatusButtonDisableOnDemand" = "Disable On-Demand";
"macToggleStatusButtonDisableOnDemandDeactivate" = "Disable On-Demand and Deactivate";
"tunnelSectionTitleInterface" = "Interface"; "tunnelSectionTitleInterface" = "Interface";
@@ -109,8 +118,9 @@
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs"; "tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@"; "tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
"tunnelOnDemandAddMessageAddNewSSID" = "Add new"; "tunnelOnDemandAddMessageAddNewSSID" = "Add new";
"tunnelOnDemandSSIDTextFieldPlaceholder" = "SSID";
"tunnelOnDemandKey" = "On demand"; "tunnelOnDemandKey" = "On-demand";
"tunnelOnDemandOptionOff" = "Off"; "tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only"; "tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular"; "tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
@@ -255,8 +265,6 @@
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor."; "alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object."; "alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
"alertTunnelDNSFailureTitle" = "DNS resolution failure"; "alertTunnelDNSFailureTitle" = "DNS resolution failure";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved."; "alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
@@ -291,19 +299,42 @@
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed."; "alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error."; "alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
// Mac status bar menu / pulldown menu // Mac status bar menu / pulldown menu / main menu
"macMenuNetworks (%@)" = "Networks: %@"; "macMenuNetworks (%@)" = "Networks: %@";
"macMenuNetworksNone" = "Networks: None"; "macMenuNetworksNone" = "Networks: None";
"macMenuTitle" = "WireGuard"; "macMenuTitle" = "WireGuard";
"macMenuManageTunnels" = "Manage tunnels"; "macTunnelsMenuTitle" = "Tunnels";
"macMenuImportTunnels" = "Import tunnel(s) from file…"; "macMenuManageTunnels" = "Manage Tunnels";
"macMenuAddEmptyTunnel" = "Add empty tunnel…"; "macMenuImportTunnels" = "Import Tunnel(s) from File…";
"macMenuViewLog" = "View log"; "macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
"macMenuExportTunnels" = "Export tunnels to zip…"; "macMenuViewLog" = "View Log";
"macMenuExportTunnels" = "Export Tunnels to Zip…";
"macMenuAbout" = "About WireGuard"; "macMenuAbout" = "About WireGuard";
"macMenuQuit" = "Quit"; "macMenuQuit" = "Quit WireGuard";
"macMenuHideApp" = "Hide WireGuard";
"macMenuHideOtherApps" = "Hide Others";
"macMenuShowAllApps" = "Show All";
"macMenuFile" = "File";
"macMenuCloseWindow" = "Close Window";
"macMenuEdit" = "Edit";
"macMenuCut" = "Cut";
"macMenuCopy" = "Copy";
"macMenuPaste" = "Paste";
"macMenuSelectAll" = "Select All";
"macMenuTunnel" = "Tunnel";
"macMenuToggleStatus" = "Toggle Status";
"macMenuEditTunnel" = "Edit…";
"macMenuDeleteSelected" = "Delete Selected";
"macMenuWindow" = "Window";
"macMenuMinimize" = "Minimize";
"macMenuZoom" = "Zoom";
// Mac manage tunnels window // Mac manage tunnels window
@@ -327,6 +358,8 @@
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels"; "macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
"macButtonEdit" = "Edit";
// Mac detail/edit view fields // Mac detail/edit view fields
"macFieldKey (%@)" = "%@:"; "macFieldKey (%@)" = "%@:";
@@ -383,10 +416,14 @@
// Mac alert // Mac alert
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.\n\nNote that if you quit WireGuard entirely the currently active tunnel ('%@') will still remain active until you deactivate it from this application or through the Network panel in System Preferences.";
"macConfirmAndQuitAlertQuitWireGuard" = "Quit WireGuard";
"macConfirmAndQuitAlertCloseWindow" = "Close Tunnels Manager";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel"; "macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences."; "macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
// Mac tooltip // Mac tooltip
@@ -411,3 +448,7 @@
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard"; "macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel %@, deactivate it, and then continue updating in App Store."; "macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel %@, deactivate it, and then continue updating in App Store.";
"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel %@ and then continue updating in App Store."; "macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel %@ and then continue updating in App Store.";
// Donation
"donateLink" = "♥ Donate to the WireGuard Project";
@@ -0,0 +1,2 @@
VERSION_NAME = 1.0.15
VERSION_ID = 26
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 953 B

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
@@ -42,50 +42,63 @@ extension ActivateOnDemandOption {
} }
} }
tunnelProviderManager.onDemandRules = rules tunnelProviderManager.onDemandRules = rules
tunnelProviderManager.isOnDemandEnabled = self != .off tunnelProviderManager.isOnDemandEnabled = (rules != nil) && tunnelProviderManager.isOnDemandEnabled
} }
init(from tunnelProviderManager: NETunnelProviderManager) { init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? [] if let onDemandRules = tunnelProviderManager.onDemandRules {
let activateOnDemandOption: ActivateOnDemandOption self = ActivateOnDemandOption.create(from: onDemandRules)
} else {
self = .off
}
}
private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
switch rules.count { switch rules.count {
case 0: case 0:
activateOnDemandOption = .off return .off
case 1: case 1:
let rule = rules[0] let rule = rules[0]
precondition(rule.action == .connect) guard rule.action == .connect else { return .off }
activateOnDemandOption = .anyInterface(.anySSID) return .anyInterface(.anySSID)
case 2: case 2:
let connectRule = rules.first(where: { $0.action == .connect })! guard let connectRule = rules.first(where: { $0.action == .connect }) else {
let disconnectRule = rules.first(where: { $0.action == .disconnect })! wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
return .off
}
guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
return .off
}
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType { if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID) return .wiFiInterfaceOnly(.anySSID)
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi { } else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .nonWiFiInterfaceOnly return .nonWiFiInterfaceOnly
} else { } else {
fatalError("Unexpected onDemandRules set on tunnel provider manager") wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
return .off
} }
case 3: case 3:
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })! guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })! guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
let ssids = ssidRule.ssidMatch! let ssids = ssidRule.ssidMatch!
switch (ssidRule.action, nonWiFiRule.action) { switch (ssidRule.action, nonWiFiRule.action) {
case (.connect, .connect): case (.connect, .connect):
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids)) return .anyInterface(.onlySpecificSSIDs(ssids))
case (.connect, .disconnect): case (.connect, .disconnect):
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids)) return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
case (.disconnect, .connect): case (.disconnect, .connect):
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids)) return .anyInterface(.exceptSpecificSSIDs(ssids))
case (.disconnect, .disconnect): case (.disconnect, .disconnect):
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids)) return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
default: default:
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager") wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
return .off
} }
default: default:
fatalError("Unexpected number of onDemandRules set on tunnel provider manager") wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
return .off
} }
self = activateOnDemandOption
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
@@ -26,11 +26,11 @@ class MockTunnels {
static func createMockTunnels() -> [NETunnelProviderManager] { static func createMockTunnels() -> [NETunnelProviderManager] {
return tunnelNames.map { tunnelName -> NETunnelProviderManager in return tunnelNames.map { tunnelName -> NETunnelProviderManager in
var interface = InterfaceConfiguration(privateKey: Curve25519.generatePrivateKey()) var interface = InterfaceConfiguration(privateKey: PrivateKey())
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!] interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
interface.dns = dnsServers.map { DNSServer(from: $0)! } interface.dns = dnsServers.map { DNSServer(from: $0)! }
var peer = PeerConfiguration(publicKey: Curve25519.generatePublicKey(fromPrivateKey: Curve25519.generatePrivateKey())) var peer = PeerConfiguration(publicKey: PrivateKey().publicKey)
peer.endpoint = Endpoint(from: endpoint) peer.endpoint = Endpoint(from: endpoint)
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!] peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -67,13 +67,14 @@ extension TunnelConfiguration {
} }
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count { if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey throw ParseError.multiplePeersWithSamePublicKey
} }
interfaceConfiguration?.addresses = base?.interface.addresses ?? [] interfaceConfiguration?.addresses = base?.interface.addresses ?? []
interfaceConfiguration?.dns = base?.interface.dns ?? [] interfaceConfiguration?.dns = base?.interface.dns ?? []
interfaceConfiguration?.dnsSearch = base?.interface.dnsSearch ?? []
interfaceConfiguration?.mtu = base?.interface.mtu interfaceConfiguration?.mtu = base?.interface.mtu
if let interfaceConfiguration = interfaceConfiguration { if let interfaceConfiguration = interfaceConfiguration {
@@ -87,7 +88,7 @@ extension TunnelConfiguration {
guard let privateKeyString = attributes["private_key"] else { guard let privateKeyString = attributes["private_key"] else {
throw ParseError.interfaceHasNoPrivateKey throw ParseError.interfaceHasNoPrivateKey
} }
guard let privateKey = Data(hexKey: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { guard let privateKey = PrivateKey(hexKey: privateKeyString) else {
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString) throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
} }
var interface = InterfaceConfiguration(privateKey: privateKey) var interface = InterfaceConfiguration(privateKey: privateKey)
@@ -106,18 +107,18 @@ extension TunnelConfiguration {
guard let publicKeyString = attributes["public_key"] else { guard let publicKeyString = attributes["public_key"] else {
throw ParseError.peerHasNoPublicKey throw ParseError.peerHasNoPublicKey
} }
guard let publicKey = Data(hexKey: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { guard let publicKey = PublicKey(hexKey: publicKeyString) else {
throw ParseError.peerHasInvalidPublicKey(publicKeyString) throw ParseError.peerHasInvalidPublicKey(publicKeyString)
} }
var peer = PeerConfiguration(publicKey: publicKey) var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["preshared_key"] { if let preSharedKeyString = attributes["preshared_key"] {
guard let preSharedKey = Data(hexKey: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { guard let preSharedKey = PreSharedKey(hexKey: preSharedKeyString) else {
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString) throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
} }
// TODO(zx2c4): does the compiler optimize this away? // TODO(zx2c4): does the compiler optimize this away?
var accumulator: UInt8 = 0 var accumulator: UInt8 = 0
for index in 0..<preSharedKey.count { for index in 0..<preSharedKey.rawValue.count {
accumulator |= preSharedKey[index] accumulator |= preSharedKey.rawValue[index]
} }
if accumulator != 0 { if accumulator != 0 {
peer.preSharedKey = preSharedKey peer.preSharedKey = preSharedKey
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import NetworkExtension import NetworkExtension
@@ -56,10 +56,10 @@ enum TunnelsManagerActivationError: WireGuardAppError {
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
case .activationFailed(let wasOnDemandEnabled): case .activationFailed:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : "")) return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled): case .activationFailedWithExtensionError(let title, let message, _):
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : "")) return (title, message)
} }
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import NetworkExtension import NetworkExtension
@@ -1,18 +1,18 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import NetworkExtension import NetworkExtension
import os.log import os.log
protocol TunnelsManagerListDelegate: class { protocol TunnelsManagerListDelegate: AnyObject {
func tunnelAdded(at index: Int) func tunnelAdded(at index: Int)
func tunnelModified(at index: Int) func tunnelModified(at index: Int)
func tunnelMoved(from oldIndex: Int, to newIndex: Int) func tunnelMoved(from oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at index: Int, tunnel: TunnelContainer) func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
} }
protocol TunnelsManagerActivationDelegate: class { protocol TunnelsManagerActivationDelegate: AnyObject {
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
@@ -23,9 +23,9 @@ class TunnelsManager {
private var tunnels: [TunnelContainer] private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate? weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate? weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject? private var statusObservationToken: NotificationToken?
private var waiteeObservationToken: AnyObject? private var waiteeObservationToken: NSKeyValueObservation?
private var configurationsObservationToken: AnyObject? private var configurationsObservationToken: NotificationToken?
init(tunnelProviders: [NETunnelProviderManager]) { init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) } tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
@@ -46,7 +46,11 @@ class TunnelsManager {
var tunnelManagers = managers ?? [] var tunnelManagers = managers ?? []
var refs: Set<Data> = [] var refs: Set<Data> = []
var tunnelNames: Set<String> = []
for (index, tunnelManager) in tunnelManagers.enumerated().reversed() { for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
if let tunnelName = tunnelManager.localizedDescription {
tunnelNames.insert(tunnelName)
}
guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue } guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue }
if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") { if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") {
tunnelManager.saveToPreferences { _ in } tunnelManager.saveToPreferences { _ in }
@@ -54,18 +58,27 @@ class TunnelsManager {
#if os(iOS) #if os(iOS)
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
#elseif os(macOS) #elseif os(macOS)
let passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying let passwordRef: Data?
if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
} else {
passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
}
#else #else
#error("Unimplemented") #error("Unimplemented")
#endif #endif
if let ref = passwordRef { if let ref = passwordRef {
refs.insert(ref) refs.insert(ref)
} else { } else {
wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
tunnelManager.removeFromPreferences { _ in } tunnelManager.removeFromPreferences { _ in }
tunnelManagers.remove(at: index) tunnelManagers.remove(at: index)
} }
} }
Keychain.deleteReferences(except: refs) Keychain.deleteReferences(except: refs)
#if os(iOS)
RecentTunnelsTracker.cleanupTunnels(except: tunnelNames)
#endif
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers))) completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
} }
#endif #endif
@@ -125,10 +138,10 @@ class TunnelsManager {
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating } let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { if let error = error {
wg_log(.error, message: "Add: Saving configuration failed: \(error!)") wg_log(.error, message: "Add: Saving configuration failed: \(error)")
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference() (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!))) completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error)))
return return
} }
@@ -156,7 +169,20 @@ class TunnelsManager {
} }
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) { func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler) // Temporarily pause observation of changes to VPN configurations to prevent the feedback
// loop that causes `reload()` to be called on each newly added tunnel, which significantly
// impacts performance.
configurationsObservationToken = nil
self.addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil) { [weak self] numSucceeded, error in
completionHandler(numSucceeded, error)
// Restart observation of changes to VPN configrations.
self?.startObservingTunnelConfigurations()
// Force reload all configurations to make sure that all tunnels are up to date.
self?.reload()
}
} }
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) { private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
@@ -180,7 +206,10 @@ class TunnelsManager {
} }
} }
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration,
onDemandOption: ActivateOnDemandOption,
shouldEnsureOnDemandEnabled: Bool = false,
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.name ?? "" let tunnelName = tunnelConfiguration.name ?? ""
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty) completionHandler(TunnelsManagerError.tunnelNameEmpty)
@@ -188,7 +217,22 @@ class TunnelsManager {
} }
let tunnelProviderManager = tunnel.tunnelProvider let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription
let isIntroducingOnDemandRules = (tunnelProviderManager.onDemandRules ?? []).isEmpty && onDemandOption != .off
if isIntroducingOnDemandRules && tunnel.status != .inactive && tunnel.status != .deactivating {
tunnel.onDeactivated = { [weak self] in
self?.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration,
onDemandOption: onDemandOption, shouldEnsureOnDemandEnabled: true,
completionHandler: completionHandler)
}
self.startDeactivation(of: tunnel)
return
} else {
tunnel.onDeactivated = nil
}
let oldName = tunnelProviderManager.localizedDescription ?? ""
let isNameChanged = tunnelName != oldName
if isNameChanged { if isNameChanged {
guard !tunnels.contains(where: { $0.name == tunnelName }) else { guard !tunnels.contains(where: { $0.name == tunnelName }) else {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName) completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
@@ -204,14 +248,17 @@ class TunnelsManager {
} }
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && shouldEnsureOnDemandEnabled
onDemandOption.apply(on: tunnelProviderManager) onDemandOption.apply(on: tunnelProviderManager)
if shouldEnsureOnDemandEnabled {
tunnelProviderManager.isOnDemandEnabled = true
}
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { if let error = error {
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back! // TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)") wg_log(.error, message: "Modify: Saving configuration failed: \(error)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!)) completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
return return
} }
guard let self = self else { return } guard let self = self else { return }
@@ -220,6 +267,9 @@ class TunnelsManager {
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) } self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
let newIndex = self.tunnels.firstIndex(of: tunnel)! let newIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex) self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
#if os(iOS)
RecentTunnelsTracker.handleTunnelRenamed(oldName: oldName, newName: tunnelName)
#endif
} }
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
@@ -236,12 +286,12 @@ class TunnelsManager {
// Without this, the tunnel stopes getting updates on the tunnel status from iOS. // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
tunnelProviderManager.loadFromPreferences { error in tunnelProviderManager.loadFromPreferences { error in
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
guard error == nil else { if let error = error {
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)") wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!)) completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
return } else {
completionHandler(nil)
} }
completionHandler(nil)
} }
} else { } else {
completionHandler(nil) completionHandler(nil)
@@ -251,14 +301,19 @@ class TunnelsManager {
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider let tunnelProviderManager = tunnel.tunnelProvider
if tunnel.isTunnelConfigurationAvailableInKeychain { #if os(macOS)
if tunnel.isTunnelAvailableToUser {
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference() (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
} }
#elseif os(iOS)
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
#else
#error("Unimplemented")
#endif
tunnelProviderManager.removeFromPreferences { [weak self] error in tunnelProviderManager.removeFromPreferences { [weak self] error in
guard error == nil else { if let error = error {
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)") wg_log(.error, message: "Remove: Saving configuration failed: \(error)")
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!)) completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error))
return return
} }
if let self = self, let index = self.tunnels.firstIndex(of: tunnel) { if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
@@ -266,11 +321,28 @@ class TunnelsManager {
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel) self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
} }
completionHandler(nil) completionHandler(nil)
#if os(iOS)
RecentTunnelsTracker.handleTunnelRemoved(tunnelName: tunnel.name)
#endif
} }
} }
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) { func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler) // Temporarily pause observation of changes to VPN configurations to prevent the feedback
// loop that causes `reload()` to be called for each removed tunnel, which significantly
// impacts performance.
configurationsObservationToken = nil
removeMultiple(tunnels: ArraySlice(tunnels)) { [weak self] error in
completionHandler(error)
// Restart observation of changes to VPN configrations.
self?.startObservingTunnelConfigurations()
// Force reload all configurations to make sure that all tunnels are up to date.
self?.reload()
}
} }
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) { private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
@@ -290,6 +362,41 @@ class TunnelsManager {
} }
} }
func setOnDemandEnabled(_ isOnDemandEnabled: Bool, on tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider
let isCurrentlyEnabled = (tunnelProviderManager.isOnDemandEnabled && tunnelProviderManager.isEnabled)
guard isCurrentlyEnabled != isOnDemandEnabled else {
completionHandler(nil)
return
}
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && isOnDemandEnabled
tunnelProviderManager.isOnDemandEnabled = isOnDemandEnabled
tunnelProviderManager.isEnabled = true
tunnelProviderManager.saveToPreferences { error in
if let error = error {
wg_log(.error, message: "Modify On-Demand: Saving configuration failed: \(error)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
return
}
if isActivatingOnDemand {
// If we're enabling on-demand, we want to make sure the tunnel is enabled.
// If not enabled, the OS will not turn the tunnel on/off based on our rules.
tunnelProviderManager.loadFromPreferences { error in
// isActivateOnDemandEnabled will get changed in reload(), but no harm in setting it here too
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
if let error = error {
wg_log(.error, message: "Modify On-Demand: Re-loading after saving configuration failed: \(error)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error))
return
}
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
func numberOfTunnels() -> Int { func numberOfTunnels() -> Int {
return tunnels.count return tunnels.count
} }
@@ -298,6 +405,10 @@ class TunnelsManager {
return tunnels[index] return tunnels[index]
} }
func mapTunnels<T>(transform: (TunnelContainer) throws -> T) rethrows -> [T] {
return try tunnels.map(transform)
}
func index(of tunnel: TunnelContainer) -> Int? { func index(of tunnel: TunnelContainer) -> Int? {
return tunnels.firstIndex(of: tunnel) return tunnels.firstIndex(of: tunnel)
} }
@@ -333,7 +444,17 @@ class TunnelsManager {
tunnel.status = .waiting tunnel.status = .waiting
activateWaitingTunnelOnDeactivation(of: tunnelInOperation) activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
if tunnelInOperation.status != .deactivating { if tunnelInOperation.status != .deactivating {
startDeactivation(of: tunnelInOperation) if tunnelInOperation.isActivateOnDemandEnabled {
setOnDemandEnabled(false, on: tunnelInOperation) { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Unable to activate tunnel '\(tunnel.name)' because on-demand could not be disabled on active tunnel '\(tunnel.name)'")
return
}
self?.startDeactivation(of: tunnelInOperation)
}
} else {
startDeactivation(of: tunnelInOperation)
}
} }
return return
} }
@@ -343,6 +464,10 @@ class TunnelsManager {
#else #else
tunnel.startActivation(activationDelegate: activationDelegate) tunnel.startActivation(activationDelegate: activationDelegate)
#endif #endif
#if os(iOS)
RecentTunnelsTracker.handleTunnelActivated(tunnelName: tunnel.name)
#endif
} }
func startDeactivation(of tunnel: TunnelContainer) { func startDeactivation(of tunnel: TunnelContainer) {
@@ -372,7 +497,7 @@ class TunnelsManager {
} }
private func startObservingTunnelStatuses() { private func startObservingTunnelStatuses() {
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in statusObservationToken = NotificationCenter.default.observe(name: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
guard let self = self, guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession, let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager, let tunnelProvider = session.manager as? NETunnelProviderManager,
@@ -394,6 +519,11 @@ class TunnelsManager {
} }
} }
if session.status == .disconnected {
tunnel.onDeactivated?()
tunnel.onDeactivated = nil
}
if tunnel.status == .restarting && session.status == .disconnected { if tunnel.status == .restarting && session.status == .disconnected {
tunnel.startActivation(activationDelegate: self.activationDelegate) tunnel.startActivation(activationDelegate: self.activationDelegate)
return return
@@ -404,7 +534,7 @@ class TunnelsManager {
} }
func startObservingTunnelConfigurations() { func startObservingTunnelConfigurations() {
configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in configurationsObservationToken = NotificationCenter.default.observe(name: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
// We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences // We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
// (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or // (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
@@ -415,8 +545,8 @@ class TunnelsManager {
} }
} }
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool { static func tunnelNameIsLessThan(_ lhs: String, _ rhs: String) -> Bool {
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending return lhs.compare(rhs, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
} }
} }
@@ -438,6 +568,7 @@ class TunnelContainer: NSObject {
@objc dynamic var status: TunnelStatus @objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool @objc dynamic var isActivateOnDemandEnabled: Bool
@objc dynamic var hasOnDemandRules: Bool
var isAttemptingActivation = false { var isAttemptingActivation = false {
didSet { didSet {
@@ -463,26 +594,35 @@ class TunnelContainer: NSObject {
var activationAttemptId: String? var activationAttemptId: String?
var activationTimer: Timer? var activationTimer: Timer?
var deactivationTimer: Timer? var deactivationTimer: Timer?
var onDeactivated: (() -> Void)?
fileprivate var tunnelProvider: NETunnelProviderManager fileprivate var tunnelProvider: NETunnelProviderManager {
didSet {
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled && tunnelProvider.isEnabled
hasOnDemandRules = !(tunnelProvider.onDemandRules ?? []).isEmpty
}
}
var tunnelConfiguration: TunnelConfiguration? { var tunnelConfiguration: TunnelConfiguration? {
return tunnelProvider.tunnelConfiguration return tunnelProvider.tunnelConfiguration
} }
var isTunnelConfigurationAvailableInKeychain: Bool {
return tunnelProvider.isTunnelConfigurationAvailableInKeychain
}
var onDemandOption: ActivateOnDemandOption { var onDemandOption: ActivateOnDemandOption {
return ActivateOnDemandOption(from: tunnelProvider) return ActivateOnDemandOption(from: tunnelProvider)
} }
#if os(macOS)
var isTunnelAvailableToUser: Bool {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
}
#endif
init(tunnel: NETunnelProviderManager) { init(tunnel: NETunnelProviderManager) {
name = tunnel.localizedDescription ?? "Unnamed" name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status) let status = TunnelStatus(from: tunnel.connection.status)
self.status = status self.status = status
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled isActivateOnDemandEnabled = tunnel.isOnDemandEnabled && tunnel.isEnabled
hasOnDemandRules = !(tunnel.onDemandRules ?? []).isEmpty
tunnelProvider = tunnel tunnelProvider = tunnel
super.init() super.init()
} }
@@ -505,11 +645,10 @@ class TunnelContainer: NSObject {
} }
func refreshStatus() { func refreshStatus() {
if status == .restarting { if (status == .restarting) || (status == .waiting && tunnelProvider.connection.status == .disconnected) {
return return
} }
status = TunnelStatus(from: tunnelProvider.connection.status) status = TunnelStatus(from: tunnelProvider.connection.status)
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
} }
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) { fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
@@ -586,18 +725,8 @@ class TunnelContainer: NSObject {
} }
extension NETunnelProviderManager { extension NETunnelProviderManager {
private static var cachedIsConfigAvailableInKeychainKey: UInt8 = 0
private static var cachedConfigKey: UInt8 = 0 private static var cachedConfigKey: UInt8 = 0
var isTunnelConfigurationAvailableInKeychain: Bool {
if let cachedNumber = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey) as? NSNumber {
return cachedNumber.boolValue
}
let isAvailable = (protocolConfiguration as? NETunnelProviderProtocol)?.verifyConfigurationReference() ?? false
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: isAvailable), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return isAvailable
}
var tunnelConfiguration: TunnelConfiguration? { var tunnelConfiguration: TunnelConfiguration? {
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration { if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
return cached return cached
@@ -613,17 +742,9 @@ extension NETunnelProviderManager {
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration) protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
localizedDescription = tunnelConfiguration.name localizedDescription = tunnelConfiguration.name
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedIsConfigAvailableInKeychainKey, NSNumber(value: true), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
} }
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool { func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
switch (isTunnelConfigurationAvailableInKeychain, tunnel.isTunnelConfigurationAvailableInKeychain) { return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
case (true, true):
return tunnelConfiguration == tunnel.tunnelConfiguration
case (false, false):
return localizedDescription == tunnel.name
default:
return false
}
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -51,20 +51,18 @@ class ActivateOnDemandViewModel {
extension ActivateOnDemandViewModel { extension ActivateOnDemandViewModel {
convenience init(tunnel: TunnelContainer) { convenience init(tunnel: TunnelContainer) {
self.init() self.init()
if tunnel.isActivateOnDemandEnabled { switch tunnel.onDemandOption {
switch tunnel.onDemandOption { case .off:
case .off: break
break case .wiFiInterfaceOnly(let onDemandSSIDOption):
case .wiFiInterfaceOnly(let onDemandSSIDOption): isWiFiInterfaceEnabled = true
isWiFiInterfaceEnabled = true (ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption) case .nonWiFiInterfaceOnly:
case .nonWiFiInterfaceOnly: isNonWiFiInterfaceEnabled = true
isNonWiFiInterfaceEnabled = true case .anyInterface(let onDemandSSIDOption):
case .anyInterface(let onDemandSSIDOption): isWiFiInterfaceEnabled = true
isWiFiInterfaceEnabled = true isNonWiFiInterfaceEnabled = true
isNonWiFiInterfaceEnabled = true (ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
}
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
protocol ErrorPresenterProtocol { protocol ErrorPresenterProtocol {
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
import LocalAuthentication import LocalAuthentication
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -44,10 +44,20 @@ class TunnelImporter {
} }
return return
} }
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName) var parseError: Error?
var tunnelConfiguration: TunnelConfiguration?
do {
tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
} catch let error {
parseError = error
}
DispatchQueue.main.async { DispatchQueue.main.async {
if tunnelConfiguration == nil { if parseError != nil {
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName)) if let parseError = parseError as? WireGuardAppError {
lastFileImportErrorText = parseError.alertText
} else {
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
}
} }
configs.append(tunnelConfiguration) configs.append(tunnelConfiguration)
dispatchGroup.leave() dispatchGroup.leave()
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation import Foundation
@@ -109,9 +109,9 @@ class TunnelViewModel {
scratchpad[field] = stringValue scratchpad[field] = stringValue
} }
if field == .privateKey { if field == .privateKey {
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength { if stringValue.count == TunnelViewModel.keyLengthInBase64,
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? "" let privateKey = PrivateKey(base64Key: stringValue) {
scratchpad[.publicKey] = publicKey scratchpad[.publicKey] = privateKey.publicKey.base64Key
} else { } else {
scratchpad.removeValue(forKey: .publicKey) scratchpad.removeValue(forKey: .publicKey)
} }
@@ -128,8 +128,8 @@ class TunnelViewModel {
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] { private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
var scratchpad = [InterfaceField: String]() var scratchpad = [InterfaceField: String]()
scratchpad[.name] = name scratchpad[.name] = name
scratchpad[.privateKey] = config.privateKey.base64Key() ?? "" scratchpad[.privateKey] = config.privateKey.base64Key
scratchpad[.publicKey] = config.publicKey.base64Key() ?? "" scratchpad[.publicKey] = config.privateKey.publicKey.base64Key
if !config.addresses.isEmpty { if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ") scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
} }
@@ -139,8 +139,10 @@ class TunnelViewModel {
if let mtu = config.mtu { if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu) scratchpad[.mtu] = String(mtu)
} }
if !config.dns.isEmpty { if !config.dns.isEmpty || !config.dnsSearch.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ") var dns = config.dns.map { $0.stringRepresentation }
dns.append(contentsOf: config.dnsSearch)
scratchpad[.dns] = dns.joined(separator: ", ")
} }
return scratchpad return scratchpad
} }
@@ -158,7 +160,7 @@ class TunnelViewModel {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired")) return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
} }
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
fieldsWithError.insert(.privateKey) fieldsWithError.insert(.privateKey)
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid")) return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
} }
@@ -194,15 +196,16 @@ class TunnelViewModel {
} }
if let dnsString = scratchpad[.dns] { if let dnsString = scratchpad[.dns] {
var dnsServers = [DNSServer]() var dnsServers = [DNSServer]()
var dnsSearch = [String]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) { for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let dnsServer = DNSServer(from: dnsServerString) { if let dnsServer = DNSServer(from: dnsServerString) {
dnsServers.append(dnsServer) dnsServers.append(dnsServer)
} else { } else {
fieldsWithError.insert(.dns) dnsSearch.append(dnsServerString)
errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
} }
} }
config.dns = dnsServers config.dns = dnsServers
config.dnsSearch = dnsSearch
} }
guard errorMessages.isEmpty else { return .error(errorMessages.first!) } guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
@@ -251,12 +254,12 @@ class TunnelViewModel {
var scratchpad = [PeerField: String]() var scratchpad = [PeerField: String]()
var fieldsWithError = Set<PeerField>() var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration? var validatedConfiguration: PeerConfiguration?
var publicKey: Data? { var publicKey: PublicKey? {
if let validatedConfiguration = validatedConfiguration { if let validatedConfiguration = validatedConfiguration {
return validatedConfiguration.publicKey return validatedConfiguration.publicKey
} }
if let scratchPadPublicKey = scratchpad[.publicKey] { if let scratchPadPublicKey = scratchpad[.publicKey] {
return Data(base64Key: scratchPadPublicKey) return PublicKey(base64Key: scratchPadPublicKey)
} }
return nil return nil
} }
@@ -301,10 +304,8 @@ class TunnelViewModel {
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] { private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
var scratchpad = [PeerField: String]() var scratchpad = [PeerField: String]()
if let publicKey = config.publicKey.base64Key() { scratchpad[.publicKey] = config.publicKey.base64Key
scratchpad[.publicKey] = publicKey if let preSharedKey = config.preSharedKey?.base64Key {
}
if let preSharedKey = config.preSharedKey?.base64Key() {
scratchpad[.preSharedKey] = preSharedKey scratchpad[.preSharedKey] = preSharedKey
} }
if !config.allowedIPs.isEmpty { if !config.allowedIPs.isEmpty {
@@ -337,14 +338,14 @@ class TunnelViewModel {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error(tr("alertInvalidPeerMessagePublicKeyRequired")) return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
} }
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { guard let publicKey = PublicKey(base64Key: publicKeyString) else {
fieldsWithError.insert(.publicKey) fieldsWithError.insert(.publicKey)
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid")) return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
} }
var config = PeerConfiguration(publicKey: publicKey) var config = PeerConfiguration(publicKey: publicKey)
var errorMessages = [String]() var errorMessages = [String]()
if let preSharedKeyString = scratchpad[.preSharedKey] { if let preSharedKeyString = scratchpad[.preSharedKey] {
if let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength { if let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) {
config.preSharedKey = preSharedKey config.preSharedKey = preSharedKey
} else { } else {
fieldsWithError.insert(.preSharedKey) fieldsWithError.insert(.preSharedKey)
@@ -397,12 +398,13 @@ class TunnelViewModel {
static let ipv4DefaultRouteString = "0.0.0.0/0" static let ipv4DefaultRouteString = "0.0.0.0/0"
static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs
"0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8", "4.0.0.0/6", "8.0.0.0/7", "11.0.0.0/8",
"64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3", "64.0.0.0/2", "128.0.0.0/3",
"172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12", "172.32.0.0/11", "172.64.0.0/10",
"176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7", "176.0.0.0/4", "192.0.0.0/9",
"192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16", "192.170.0.0/15",
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4" "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10", "193.0.0.0/8",
"194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
] ]
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) { static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
@@ -559,7 +561,7 @@ class TunnelViewModel {
} }
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey } let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray) let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count { if peerPublicKeysArray.count != peerPublicKeysSet.count {
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated")) return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
@@ -9,12 +9,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var mainVC: MainViewController? var mainVC: MainViewController?
var isLaunchedForSpecificAction = false
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path) Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
if let launchOptions = launchOptions {
if launchOptions[.url] != nil || launchOptions[.shortcutItem] != nil {
isLaunchedForSpecificAction = true
}
}
let window = UIWindow(frame: UIScreen.main.bounds) let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = .white
self.window = window self.window = window
let mainVC = MainViewController() let mainVC = MainViewController()
@@ -27,16 +33,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
guard let tunnelsManager = mainVC?.tunnelsManager else { return true } mainVC?.importFromDisposableFile(url: url)
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
_ = FileManager.deleteFile(at: url)
}
return true return true
} }
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
mainVC?.refreshTunnelConnectionStatuses() mainVC?.refreshTunnelConnectionStatuses()
} }
func applicationWillResignActive(_ application: UIApplication) {
guard let allTunnelNames = mainVC?.allTunnelNames() else { return }
application.shortcutItems = QuickActionItem.createItems(allTunnelNames: allTunnelNames)
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
guard shortcutItem.type == QuickActionItem.type else {
completionHandler(false)
return
}
let tunnelName = shortcutItem.localizedTitle
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: true)
completionHandler(true)
}
} }
extension AppDelegate { extension AppDelegate {
@@ -45,7 +63,7 @@ extension AppDelegate {
} }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true return !self.isLaunchedForSpecificAction
} }
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? { func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
@@ -58,7 +76,7 @@ extension AppDelegate {
} }
} else { } else {
// Show it when tunnelsManager is available // Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false) mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: false)
} }
} }
return nil return nil
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
@@ -64,6 +64,8 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
class QuickActionItem: UIApplicationShortcutItem {
static let type = "WireGuardTunnelActivateAndShow"
init(tunnelName: String) {
super.init(type: QuickActionItem.type, localizedTitle: tunnelName, localizedSubtitle: nil, icon: nil, userInfo: nil)
}
static func createItems(allTunnelNames: [String]) -> [QuickActionItem] {
let numberOfItems = 10
// Currently, only 4 items shown by iOS, but that can increase in the future.
// iOS will discard additional items we give it.
var tunnelNames = RecentTunnelsTracker.recentlyActivatedTunnelNames(limit: numberOfItems)
let numberOfSlotsRemaining = numberOfItems - tunnelNames.count
if numberOfSlotsRemaining > 0 {
let moreTunnels = allTunnelNames.filter { !tunnelNames.contains($0) }.prefix(numberOfSlotsRemaining)
tunnelNames.append(contentsOf: moreTunnels)
}
return tunnelNames.map { QuickActionItem(tunnelName: $0) }
}
}
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Foundation
class RecentTunnelsTracker {
private static let keyRecentlyActivatedTunnelNames = "recentlyActivatedTunnelNames"
private static let maxNumberOfTunnels = 10
private static var userDefaults: UserDefaults? {
guard let appGroupId = FileManager.appGroupId else {
wg_log(.error, staticMessage: "Cannot obtain app group ID from bundle for tracking recently used tunnels")
return nil
}
guard let userDefaults = UserDefaults(suiteName: appGroupId) else {
wg_log(.error, staticMessage: "Cannot obtain shared user defaults for tracking recently used tunnels")
return nil
}
return userDefaults
}
static func handleTunnelActivated(tunnelName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
recentTunnels.remove(at: existingIndex)
}
recentTunnels.insert(tunnelName, at: 0)
if recentTunnels.count > maxNumberOfTunnels {
recentTunnels.removeLast(recentTunnels.count - maxNumberOfTunnels)
}
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
static func handleTunnelRemoved(tunnelName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
recentTunnels.remove(at: existingIndex)
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func handleTunnelRenamed(oldName: String, newName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: oldName) {
recentTunnels[existingIndex] = newName
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func cleanupTunnels(except tunnelNamesToKeep: Set<String>) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
let oldCount = recentTunnels.count
recentTunnels.removeAll { !tunnelNamesToKeep.contains($0) }
if oldCount != recentTunnels.count {
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func recentlyActivatedTunnelNames(limit: Int) -> [String] {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return [] }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if limit < recentTunnels.count {
recentTunnels.removeLast(recentTunnels.count - limit)
}
return recentTunnels
}
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -15,7 +15,7 @@ extension UITableView {
} }
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T { func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
//swiftlint:disable:next force_cast // swiftlint:disable:next force_cast
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -9,8 +9,8 @@ class ButtonCell: UITableViewCell {
set(value) { button.setTitle(value, for: .normal) } set(value) { button.setTitle(value, for: .normal) }
} }
var hasDestructiveAction: Bool { var hasDestructiveAction: Bool {
get { return button.tintColor == .red } get { return button.tintColor == .systemRed }
set(value) { button.tintColor = value ? .red : buttonStandardTintColor } set(value) { button.tintColor = value ? .systemRed : buttonStandardTintColor }
} }
var onTapped: (() -> Void)? var onTapped: (() -> Void)?
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -9,6 +9,11 @@ class EditableTextCell: UITableViewCell {
set(value) { valueTextField.text = value } set(value) { valueTextField.text = value }
} }
var placeholder: String? {
get { return valueTextField.placeholder }
set(value) { valueTextField.placeholder = value }
}
let valueTextField: UITextField = { let valueTextField: UITextField = {
let valueTextField = UITextField() let valueTextField = UITextField()
valueTextField.textAlignment = .left valueTextField.textAlignment = .left
@@ -29,12 +34,13 @@ class EditableTextCell: UITableViewCell {
valueTextField.delegate = self valueTextField.delegate = self
contentView.addSubview(valueTextField) contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false valueTextField.translatesAutoresizingMaskIntoConstraints = false
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1) // Reduce the bottom margin by 0.5pt to maintain the default cell height (44pt)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: valueTextField.bottomAnchor, constant: -0.5)
bottomAnchorConstraint.priority = .defaultLow bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1), valueTextField.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1), contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: valueTextField.trailingAnchor),
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1), contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: valueTextField.topAnchor),
bottomAnchorConstraint bottomAnchorConstraint
]) ])
} }
@@ -50,6 +56,7 @@ class EditableTextCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
message = "" message = ""
placeholder = nil
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -9,7 +9,11 @@ class KeyValueCell: UITableViewCell {
let keyLabel = UILabel() let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body) keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true keyLabel.adjustsFontForContentSizeCategory = true
keyLabel.textColor = .black if #available(iOS 13.0, *) {
keyLabel.textColor = .label
} else {
keyLabel.textColor = .black
}
keyLabel.textAlignment = .left keyLabel.textAlignment = .left
return keyLabel return keyLabel
}() }()
@@ -23,7 +27,7 @@ class KeyValueCell: UITableViewCell {
}() }()
let valueTextField: UITextField = { let valueTextField: UITextField = {
let valueTextField = UITextField() let valueTextField = KeyValueCellTextField()
valueTextField.textAlignment = .right valueTextField.textAlignment = .right
valueTextField.isEnabled = false valueTextField.isEnabled = false
valueTextField.font = UIFont.preferredFont(forTextStyle: .body) valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
@@ -31,7 +35,11 @@ class KeyValueCell: UITableViewCell {
valueTextField.autocapitalizationType = .none valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no valueTextField.spellCheckingType = .no
valueTextField.textColor = .gray if #available(iOS 13.0, *) {
valueTextField.textColor = .secondaryLabel
} else {
valueTextField.textColor = .gray
}
return valueTextField return valueTextField
}() }()
@@ -56,10 +64,18 @@ class KeyValueCell: UITableViewCell {
var isValueValid = true { var isValueValid = true {
didSet { didSet {
if isValueValid { if #available(iOS 13.0, *) {
keyLabel.textColor = .black if isValueValid {
keyLabel.textColor = .label
} else {
keyLabel.textColor = .systemRed
}
} else { } else {
keyLabel.textColor = .red if isValueValid {
keyLabel.textColor = .black
} else {
keyLabel.textColor = .red
}
} }
} }
} }
@@ -99,8 +115,6 @@ class KeyValueCell: UITableViewCell {
expandToFitValueLabelConstraint.priority = .defaultLow + 1 expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true expandToFitValueLabelConstraint.isActive = true
contentView.addSubview(valueLabelScrollView)
contentView.addSubview(valueLabelScrollView) contentView.addSubview(valueLabelScrollView)
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@@ -218,3 +232,10 @@ extension KeyValueCell: UITextFieldDelegate {
} }
} }
class KeyValueCellTextField: UITextField {
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
// UIKit renders the placeholder label 0.5pt higher
return super.placeholderRect(forBounds: bounds).integral.offsetBy(dx: 0, dy: -0.5)
}
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -16,13 +16,19 @@ class SwitchCell: UITableViewCell {
get { return switchView.isEnabled } get { return switchView.isEnabled }
set(value) { set(value) {
switchView.isEnabled = value switchView.isEnabled = value
textLabel?.textColor = value ? .black : .gray if #available(iOS 13.0, *) {
textLabel?.textColor = value ? .label : .secondaryLabel
} else {
textLabel?.textColor = value ? .black : .gray
}
} }
} }
var onSwitchToggled: ((Bool) -> Void)? var onSwitchToggled: ((Bool) -> Void)?
var observationToken: AnyObject? var statusObservationToken: AnyObject?
var isOnDemandEnabledObservationToken: AnyObject?
var hasOnDemandRulesObservationToken: AnyObject?
let switchView = UISwitch() let switchView = UISwitch()
@@ -47,6 +53,8 @@ class SwitchCell: UITableViewCell {
isEnabled = true isEnabled = true
message = "" message = ""
isOn = false isOn = false
observationToken = nil statusObservationToken = nil
isOnDemandEnabledObservationToken = nil
hasOnDemandRulesObservationToken = nil
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -28,7 +28,11 @@ class TextCell: UITableViewCell {
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
message = "" message = ""
setTextColor(.black) if #available(iOS 13.0, *) {
setTextColor(.label)
} else {
setTextColor(.black)
}
setTextAlignment(.left) setTextAlignment(.left)
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -30,7 +30,11 @@ class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
copyableGesture = false copyableGesture = false
valueTextField.textColor = .black if #available(iOS 13.0, *) {
valueTextField.textColor = .label
} else {
valueTextField.textColor = .black
}
valueTextField.isEnabled = true valueTextField.isEnabled = true
valueLabelScrollView.isScrollEnabled = false valueLabelScrollView.isScrollEnabled = false
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
@@ -0,0 +1,170 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelListCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel, animated: false)
statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel, animated: true)
}
// Bind to tunnel's on-demand settings
isOnDemandEnabledObservationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
self?.update(from: tunnel, animated: true)
}
hasOnDemandRulesObservationToken = tunnel?.observe(\.hasOnDemandRules) { [weak self] tunnel, _ in
self?.update(from: tunnel, animated: true)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel = {
let nameLabel = UILabel()
nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
nameLabel.adjustsFontForContentSizeCategory = true
nameLabel.numberOfLines = 0
return nameLabel
}()
let onDemandLabel: UILabel = {
let label = UILabel()
label.text = ""
label.font = UIFont.preferredFont(forTextStyle: .caption2)
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 1
if #available(iOS 13.0, *) {
label.textColor = .secondaryLabel
} else {
label.textColor = .gray
}
return label
}()
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator: UIActivityIndicatorView
if #available(iOS 13.0, *) {
busyIndicator = UIActivityIndicatorView(style: .medium)
} else {
busyIndicator = UIActivityIndicatorView(style: .gray)
}
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
let statusSwitch = UISwitch()
private var nameObservationToken: NSKeyValueObservation?
private var statusObservationToken: NSKeyValueObservation?
private var isOnDemandEnabledObservationToken: NSKeyValueObservation?
private var hasOnDemandRulesObservationToken: NSKeyValueObservation?
private var subTitleLabelBottomConstraint: NSLayoutConstraint?
private var nameLabelBottomConstraint: NSLayoutConstraint?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
for subview in [statusSwitch, busyIndicator, onDemandLabel, nameLabel] {
subview.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(subview)
}
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
onDemandLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
let nameLabelBottomConstraint =
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
nameLabelBottomConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1),
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: onDemandLabel.trailingAnchor, multiplier: 1),
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
nameLabel.trailingAnchor.constraint(lessThanOrEqualTo: statusSwitch.leadingAnchor),
nameLabelBottomConstraint,
onDemandLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
onDemandLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1),
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
busyIndicator.leadingAnchor.constraint(greaterThanOrEqualToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1)
])
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
reset(animated: false)
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
statusSwitch.isEnabled = !editing
}
@objc private func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from tunnel: TunnelContainer?, animated: Bool) {
guard let tunnel = tunnel else {
reset(animated: animated)
return
}
let status = tunnel.status
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
let shouldSwitchBeOn = ((status != .deactivating && status != .inactive) || isOnDemandEngaged)
statusSwitch.setOn(shouldSwitchBeOn, animated: true)
if isOnDemandEngaged && !(status == .activating || status == .active) {
statusSwitch.onTintColor = UIColor.systemYellow
} else {
statusSwitch.onTintColor = UIColor.systemGreen
}
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if tunnel.hasOnDemandRules {
onDemandLabel.text = isOnDemandEngaged ? tr("tunnelListCaptionOnDemand") : ""
busyIndicator.stopAnimating()
statusSwitch.isUserInteractionEnabled = true
} else {
onDemandLabel.text = ""
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
}
}
private func reset(animated: Bool) {
statusSwitch.thumbTintColor = nil
statusSwitch.setOn(false, animated: animated)
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -15,18 +15,37 @@ class LogViewController: UIViewController {
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) if #available(iOS 13.0, *) {
busyIndicator.hidesWhenStopped = true let busyIndicator = UIActivityIndicatorView(style: .medium)
return busyIndicator busyIndicator.hidesWhenStopped = true
return busyIndicator
} else {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}
}() }()
let paragraphStyle: NSParagraphStyle = {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.setParagraphStyle(NSParagraphStyle.default)
paragraphStyle.lineHeightMultiple = 1.2
return paragraphStyle
}()
var isNextLineHighlighted = false
var logViewHelper: LogViewHelper? var logViewHelper: LogViewHelper?
var isFetchingLogEntries = false var isFetchingLogEntries = false
private var updateLogEntriesTimer: Timer? private var updateLogEntriesTimer: Timer?
override func loadView() { override func loadView() {
view = UIView() view = UIView()
view.backgroundColor = .white if #available(iOS 13.0, *) {
view.backgroundColor = .systemBackground
} else {
view.backgroundColor = .white
}
view.addSubview(textView) view.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false textView.translatesAutoresizingMaskIntoConstraints = false
@@ -68,9 +87,26 @@ class LogViewController: UIViewController {
} }
guard !fetchedLogEntries.isEmpty else { return } guard !fetchedLogEntries.isEmpty else { return }
let isScrolledToEnd = self.textView.contentSize.height - self.textView.bounds.height - self.textView.contentOffset.y < 1 let isScrolledToEnd = self.textView.contentSize.height - self.textView.bounds.height - self.textView.contentOffset.y < 1
let text = fetchedLogEntries.reduce("") { $0 + $1.text() + "\n" }
let font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body) let richText = NSMutableAttributedString()
let richText = NSAttributedString(string: text, attributes: [.font: font]) let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
for logEntry in fetchedLogEntries {
var bgColor: UIColor
var fgColor: UIColor
if #available(iOS 13.0, *) {
bgColor = self.isNextLineHighlighted ? .systemGray3 : .systemBackground
fgColor = .label
} else {
bgColor = self.isNextLineHighlighted ? UIColor(white: 0.88, alpha: 1.0) : UIColor.white
fgColor = .black
}
let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
richText.append(timestampText)
richText.append(messageText)
self.isNextLineHighlighted.toggle()
}
self.textView.textStorage.append(richText) self.textView.textStorage.append(richText)
if isScrolledToEnd { if isScrolledToEnd {
let endOfCurrentText = NSRange(location: (self.textView.text as NSString).length, length: 0) let endOfCurrentText = NSRange(location: (self.textView.text as NSString).length, length: 0)
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -11,7 +11,11 @@ class MainViewController: UISplitViewController {
init() { init() {
let detailVC = UIViewController() let detailVC = UIViewController()
detailVC.view.backgroundColor = .white if #available(iOS 13.0, *) {
detailVC.view.backgroundColor = .systemBackground
} else {
detailVC.view.backgroundColor = .white
}
let detailNC = UINavigationController(rootViewController: detailVC) let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController() let masterVC = TunnelsListTableViewController()
@@ -57,6 +61,10 @@ class MainViewController: UISplitViewController {
} }
} }
func allTunnelNames() -> [String]? {
guard let tunnelsManager = self.tunnelsManager else { return nil }
return tunnelsManager.mapTunnels { $0.name }
}
} }
extension MainViewController: TunnelsManagerActivationDelegate { extension MainViewController: TunnelsManagerActivationDelegate {
@@ -84,19 +92,17 @@ extension MainViewController {
} }
} }
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) { func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool, shouldToggleStatus: Bool) {
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
guard let self = self else { return }
guard let tunnelsListVC = self.tunnelsListVC else { return }
if let tunnel = tunnelsManager.tunnel(named: tunnelName) { if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) tunnelsListVC.showTunnelDetail(for: tunnel, animated: false)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) if shouldToggleStatus {
tunnelDetailNC.restorationIdentifier = "DetailNC" if tunnel.status == .inactive {
if let self = self { tunnelsManager.startActivation(of: tunnel)
if animated { } else if tunnel.status == .active {
self.showDetailViewController(tunnelDetailNC, sender: self) tunnelsManager.startDeactivation(of: tunnel)
} else {
UIView.performWithoutAnimation {
self.showDetailViewController(tunnelDetailNC, sender: self)
}
} }
} }
} }
@@ -107,6 +113,19 @@ extension MainViewController {
onTunnelsManagerReady = showTunnelDetailBlock onTunnelsManagerReady = showTunnelDetailBlock
} }
} }
func importFromDisposableFile(url: URL) {
let importFromFileBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) {
_ = FileManager.deleteFile(at: url)
}
}
if let tunnelsManager = tunnelsManager {
importFromFileBlock(tunnelsManager)
} else {
onTunnelsManagerReady = importFromFileBlock
}
}
} }
extension MainViewController: UISplitViewControllerDelegate { extension MainViewController: UISplitViewControllerDelegate {
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import AVFoundation import AVFoundation
import UIKit import UIKit
protocol QRScanViewControllerDelegate: class { protocol QRScanViewControllerDelegate: AnyObject {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?) func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?)
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -1,10 +1,11 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import SystemConfiguration.CaptiveNetwork import SystemConfiguration.CaptiveNetwork
import NetworkExtension
protocol SSIDOptionEditTableViewControllerDelegate: class { protocol SSIDOptionEditTableViewControllerDelegate: AnyObject {
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
} }
@@ -39,9 +40,16 @@ class SSIDOptionEditTableViewController: UITableViewController {
selectedOption = option selectedOption = option
selectedSSIDs = ssids selectedSSIDs = ssids
super.init(style: .grouped) super.init(style: .grouped)
connectedSSID = getConnectedSSID()
loadSections() loadSections()
loadAddSSIDRows() addSSIDRows.removeAll()
addSSIDRows.append(.addNewSSID)
getConnectedSSID { [weak self] ssid in
guard let self = self else { return }
self.connectedSSID = ssid
self.updateCurrentSSIDEntry()
self.updateTableViewAddSSIDRows()
}
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@@ -60,6 +68,7 @@ class SSIDOptionEditTableViewController: UITableViewController {
tableView.register(TextCell.self) tableView.register(TextCell.self)
tableView.isEditing = true tableView.isEditing = true
tableView.allowsSelectionDuringEditing = true tableView.allowsSelectionDuringEditing = true
tableView.keyboardDismissMode = .onDrag
} }
func loadSections() { func loadSections() {
@@ -71,14 +80,14 @@ class SSIDOptionEditTableViewController: UITableViewController {
} }
} }
func loadAddSSIDRows() { func updateCurrentSSIDEntry() {
addSSIDRows.removeAll() if let connectedSSID = connectedSSID, !selectedSSIDs.contains(connectedSSID) {
if let connectedSSID = connectedSSID { if let first = addSSIDRows.first, case .addNewSSID = first {
if !selectedSSIDs.contains(connectedSSID) { addSSIDRows.insert(.addConnectedSSID(connectedSSID: connectedSSID), at: 0)
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
} }
} else if let first = addSSIDRows.first, case .addConnectedSSID = first {
addSSIDRows.removeFirst()
} }
addSSIDRows.append(.addNewSSID)
} }
func updateTableViewAddSSIDRows() { func updateTableViewAddSSIDRows() {
@@ -176,7 +185,11 @@ extension SSIDOptionEditTableViewController {
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath) let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = tr("tunnelOnDemandNoSSIDs") cell.message = tr("tunnelOnDemandNoSSIDs")
cell.setTextColor(.gray) if #available(iOS 13.0, *) {
cell.setTextColor(.secondaryLabel)
} else {
cell.setTextColor(.gray)
}
cell.setTextAlignment(.center) cell.setTextAlignment(.center)
return cell return cell
} }
@@ -184,12 +197,13 @@ extension SSIDOptionEditTableViewController {
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath) let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = selectedSSIDs[indexPath.row] cell.message = selectedSSIDs[indexPath.row]
cell.placeholder = tr("tunnelOnDemandSSIDTextFieldPlaceholder")
cell.isEditing = true cell.isEditing = true
cell.onValueBeingEdited = { [weak self, weak cell] text in cell.onValueBeingEdited = { [weak self, weak cell] text in
guard let self = self, let cell = cell else { return } guard let self = self, let cell = cell else { return }
if let row = self.tableView.indexPath(for: cell)?.row { if let row = self.tableView.indexPath(for: cell)?.row {
self.selectedSSIDs[row] = text self.selectedSSIDs[row] = text
self.loadAddSSIDRows() self.updateCurrentSSIDEntry()
self.updateTableViewAddSSIDRows() self.updateTableViewAddSSIDRows()
} }
} }
@@ -220,7 +234,7 @@ extension SSIDOptionEditTableViewController {
} else { } else {
tableView.reloadRows(at: [indexPath], with: .automatic) tableView.reloadRows(at: [indexPath], with: .automatic)
} }
loadAddSSIDRows() updateCurrentSSIDEntry()
updateTableViewAddSSIDRows() updateTableViewAddSSIDRows()
case .addSSIDs: case .addSSIDs:
assert(editingStyle == .insert) assert(editingStyle == .insert)
@@ -240,7 +254,7 @@ extension SSIDOptionEditTableViewController {
} else { } else {
tableView.insertRows(at: [indexPath], with: .automatic) tableView.insertRows(at: [indexPath], with: .automatic)
} }
loadAddSSIDRows() updateCurrentSSIDEntry()
updateTableViewAddSSIDRows() updateTableViewAddSSIDRows()
if newSSID.isEmpty { if newSSID.isEmpty {
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell { if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
@@ -249,6 +263,31 @@ extension SSIDOptionEditTableViewController {
} }
} }
} }
private func getConnectedSSID(completionHandler: @escaping (String?) -> Void) {
#if targetEnvironment(simulator)
completionHandler("Simulator Wi-Fi")
#else
if #available(iOS 14, *) {
NEHotspotNetwork.fetchCurrent { hotspotNetwork in
completionHandler(hotspotNetwork?.ssid)
}
} else {
if let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] {
for interface in supportedInterfaces {
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
completionHandler(!ssid.isEmpty ? ssid : nil)
return
}
}
}
}
completionHandler(nil)
}
#endif
}
} }
extension SSIDOptionEditTableViewController { extension SSIDOptionEditTableViewController {
@@ -266,6 +305,10 @@ extension SSIDOptionEditTableViewController {
case .ssidOption: case .ssidOption:
let previousOption = selectedOption let previousOption = selectedOption
selectedOption = ssidOptionFields[indexPath.row] selectedOption = ssidOptionFields[indexPath.row]
guard previousOption != selectedOption else {
tableView.deselectRow(at: indexPath, animated: true)
return
}
loadSections() loadSections()
if previousOption == .anySSID { if previousOption == .anySSID {
let indexSet = IndexSet(1 ... 2) let indexSet = IndexSet(1 ... 2)
@@ -281,15 +324,3 @@ extension SSIDOptionEditTableViewController {
} }
} }
} }
private func getConnectedSSID() -> String? {
guard let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] else { return nil }
for interface in supportedInterfaces {
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
return !ssid.isEmpty ? ssid : nil
}
}
}
return nil
}
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import os.log import os.log
@@ -144,8 +144,8 @@ extension SettingsTableViewController {
cell.copyableGesture = false cell.copyableGesture = false
cell.key = field.localizedUIString cell.key = field.localizedUIString
if field == .iosAppVersion { if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version" var appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { if let appBuild = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String {
appVersion += " (\(appBuild))" appVersion += " (\(appBuild))"
} }
cell.value = appVersion cell.value = appVersion
@@ -160,8 +160,7 @@ extension SettingsTableViewController {
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
} }
return cell return cell
} else { } else if field == .viewLog {
assert(field == .viewLog)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.localizedUIString cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
@@ -169,5 +168,6 @@ extension SettingsTableViewController {
} }
return cell return cell
} }
fatalError()
} }
} }
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
@@ -120,7 +120,7 @@ class TunnelDetailTableViewController: UITableViewController {
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel) let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
editVC.delegate = self editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet editNC.modalPresentationStyle = .fullScreen
self.present(editNC, animated: true) self.present(editNC, animated: true)
} }
} }
@@ -152,8 +152,8 @@ class TunnelDetailTableViewController: UITableViewController {
} }
}! }!
let firstPeerSectionIndex = interfaceSectionIndex + 1 let firstPeerSectionIndex = interfaceSectionIndex + 1
var interfaceFieldIsVisible = self.interfaceFieldIsVisible let interfaceFieldIsVisible = self.interfaceFieldIsVisible
var peerFieldIsVisible = self.peerFieldIsVisible let peerFieldIsVisible = self.peerFieldIsVisible
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) { func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
for (index, field) in fields.enumerated() { for (index, field) in fields.enumerated() {
@@ -324,8 +324,22 @@ extension TunnelDetailTableViewController {
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in func update(cell: SwitchCell?, with tunnel: TunnelContainer) {
let text: String guard let cell = cell else { return }
let status = tunnel.status
let isOnDemandEngaged = tunnel.isActivateOnDemandEnabled
let isSwitchOn = (status == .activating || status == .active || isOnDemandEngaged)
cell.switchView.setOn(isSwitchOn, animated: true)
if isOnDemandEngaged && !(status == .activating || status == .active) {
cell.switchView.onTintColor = UIColor.systemYellow
} else {
cell.switchView.onTintColor = UIColor.systemGreen
}
var text: String
switch status { switch status {
case .inactive: case .inactive:
text = tr("tunnelStatusInactive") text = tr("tunnelStatusInactive")
@@ -342,26 +356,49 @@ extension TunnelDetailTableViewController {
case .waiting: case .waiting:
text = tr("tunnelStatusWaiting") text = tr("tunnelStatusWaiting")
} }
cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in if tunnel.hasOnDemandRules {
cell?.switchView.isOn = !(status == .deactivating || status == .inactive) text += isOnDemandEngaged ? tr("tunnelStatusAddendumOnDemand") : ""
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active) cell.switchView.isUserInteractionEnabled = true
cell.isEnabled = true
} else {
cell.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
cell.isEnabled = (status == .inactive || status == .active)
} }
cell.isEnabled = status == .active || status == .inactive
if tunnel.hasOnDemandRules && !isOnDemandEngaged && status == .inactive {
text = tr("tunnelStatusOnDemandDisabled")
}
cell.textLabel?.text = text
} }
statusUpdate(cell, tunnel.status) update(cell: cell, with: tunnel)
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in cell.statusObservationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return } update(cell: cell, with: tunnel)
statusUpdate(cell, tunnel.status) }
cell.isOnDemandEnabledObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
update(cell: cell, with: tunnel)
}
cell.hasOnDemandRulesObservationToken = tunnel.observe(\.hasOnDemandRules) { [weak cell] tunnel, _ in
update(cell: cell, with: tunnel)
} }
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return } guard let self = self else { return }
if isOn {
self.tunnelsManager.startActivation(of: self.tunnel) if self.tunnel.hasOnDemandRules {
self.tunnelsManager.setOnDemandEnabled(isOn, on: self.tunnel) { error in
if error == nil && !isOn {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
} else { } else {
self.tunnelsManager.startDeactivation(of: self.tunnel) if isOn {
self.tunnelsManager.startActivation(of: self.tunnel)
} else {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
} }
} }
return cell return cell
@@ -397,6 +434,7 @@ extension TunnelDetailTableViewController {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString cell.key = field.localizedUIString
cell.value = onDemandViewModel.localizedInterfaceDescription cell.value = onDemandViewModel.localizedInterfaceDescription
cell.copyableGesture = false
return cell return cell
} else { } else {
assert(field == .ssid) assert(field == .ssid)
@@ -404,6 +442,7 @@ extension TunnelDetailTableViewController {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString cell.key = field.localizedUIString
cell.value = onDemandViewModel.ssidOption.localizedUIString cell.value = onDemandViewModel.ssidOption.localizedUIString
cell.copyableGesture = false
return cell return cell
} else { } else {
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath) let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
protocol TunnelEditTableViewControllerDelegate: class { protocol TunnelEditTableViewControllerDelegate: AnyObject {
func tunnelSaved(tunnel: TunnelContainer) func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled() func tunnelEditingCancelled()
} }
@@ -214,7 +214,7 @@ extension TunnelEditTableViewController {
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64Key() ?? "" self.tunnelViewModel.interfaceData[.privateKey] = PrivateKey().base64Key
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey), if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) { let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section) let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
@@ -266,7 +266,7 @@ extension TunnelEditTableViewController {
guard let self = self else { return } guard let self = self else { return }
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue) let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
if isAllowedIPsChanged { if isAllowedIPsChanged {
let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } } let section = self.sections.firstIndex { if case .peer = $0 { return true } else { return false } }
if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) { if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none) self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
} }
@@ -318,7 +318,7 @@ extension TunnelEditTableViewController {
let removedSectionIndices = self.deletePeer(peer: peerData) let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
//swiftlint:disable:next trailing_closure // swiftlint:disable:next trailing_closure
tableView.performBatchUpdates({ tableView.performBatchUpdates({
self.tableView.deleteSections(removedSectionIndices, with: .fade) self.tableView.deleteSections(removedSectionIndices, with: .fade)
if shouldShowExcludePrivateIPs { if shouldShowExcludePrivateIPs {
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import UIKit import UIKit
import MobileCoreServices import MobileCoreServices
@@ -32,7 +32,12 @@ class TunnelsListTableViewController: UIViewController {
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) let busyIndicator: UIActivityIndicatorView
if #available(iOS 13.0, *) {
busyIndicator = UIActivityIndicatorView(style: .medium)
} else {
busyIndicator = UIActivityIndicatorView(style: .gray)
}
busyIndicator.hidesWhenStopped = true busyIndicator.hidesWhenStopped = true
return busyIndicator return busyIndicator
}() }()
@@ -46,7 +51,11 @@ class TunnelsListTableViewController: UIViewController {
override func loadView() { override func loadView() {
view = UIView() view = UIView()
view.backgroundColor = .white if #available(iOS 13.0, *) {
view.backgroundColor = .systemBackground
} else {
view.backgroundColor = .white
}
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
@@ -178,7 +187,7 @@ class TunnelsListTableViewController: UIViewController {
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) { func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager) let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet editNC.modalPresentationStyle = .fullScreen
present(editNC, animated: true) present(editNC, animated: true)
} }
@@ -251,6 +260,24 @@ class TunnelsListTableViewController: UIViewController {
} }
} }
} }
func showTunnelDetail(for tunnel: TunnelContainer, animated: Bool) {
guard let tunnelsManager = tunnelsManager else { return }
guard let splitViewController = splitViewController else { return }
guard let navController = navigationController else { return }
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
if splitViewController.isCollapsed && navController.viewControllers.count > 1 {
navController.setViewControllers([self, tunnelDetailNC], animated: animated)
} else {
splitViewController.showDetailViewController(tunnelDetailNC, sender: self, animated: animated)
}
detailDisplayedTunnel = tunnel
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
} }
extension TunnelsListTableViewController: UIDocumentPickerDelegate { extension TunnelsListTableViewController: UIDocumentPickerDelegate {
@@ -290,10 +317,18 @@ extension TunnelsListTableViewController: UITableViewDataSource {
cell.tunnel = tunnel cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self, let tunnelsManager = self.tunnelsManager else { return } guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
if isOn { if tunnel.hasOnDemandRules {
tunnelsManager.startActivation(of: tunnel) tunnelsManager.setOnDemandEnabled(isOn, on: tunnel) { error in
if error == nil && !isOn {
tunnelsManager.startDeactivation(of: tunnel)
}
}
} else { } else {
tunnelsManager.startDeactivation(of: tunnel) if isOn {
tunnelsManager.startActivation(of: tunnel)
} else {
tunnelsManager.startDeactivation(of: tunnel)
}
} }
} }
} }
@@ -309,12 +344,7 @@ extension TunnelsListTableViewController: UITableViewDelegate {
} }
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, showTunnelDetail(for: tunnel, animated: true)
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
detailDisplayedTunnel = tunnel
} }
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
@@ -376,7 +406,11 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
(splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false) (splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false)
} else { } else {
let detailVC = UIViewController() let detailVC = UIViewController()
detailVC.view.backgroundColor = .white if #available(iOS 13.0, *) {
detailVC.view.backgroundColor = .systemBackground
} else {
detailVC.view.backgroundColor = .white
}
let detailNC = UINavigationController(rootViewController: detailVC) let detailNC = UINavigationController(rootViewController: detailVC)
splitViewController.showDetailViewController(detailNC, sender: self) splitViewController.showDetailViewController(detailNC, sender: self)
} }
@@ -387,3 +421,15 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
} }
} }
} }
extension UISplitViewController {
func showDetailViewController(_ viewController: UIViewController, sender: Any?, animated: Bool) {
if animated {
showDetailViewController(viewController, sender: sender)
} else {
UIView.performWithoutAnimation {
showDetailViewController(viewController, sender: sender)
}
}
}
}
@@ -0,0 +1,236 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
import ServiceManagement
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var tunnelsManager: TunnelsManager?
var tunnelsTracker: TunnelsTracker?
var statusItemController: StatusItemController?
var manageTunnelsRootVC: ManageTunnelsRootViewController?
var manageTunnelsWindowObject: NSWindow?
var onAppDeactivation: (() -> Void)?
func applicationWillFinishLaunching(_ notification: Notification) {
// To workaround a possible AppKit bug that causes the main menu to become unresponsive sometimes
// (especially when launched through Xcode) if we call setActivationPolicy(.regular) in
// in applicationDidFinishLaunching, we set it to .prohibited here.
// Setting it to .regular would fix that problem too, but at this point, we don't know
// whether the app was launched at login or not, so we're not sure whether we should
// show the app icon in the dock or not.
NSApp.setActivationPolicy(.prohibited)
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
registerLoginItem(shouldLaunchAtLogin: true)
var isLaunchedAtLogin = false
if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
isLaunchedAtLogin = LaunchedAtLoginDetector.isLaunchedAtLogin(openAppleEvent: appleEvent)
}
NSApp.mainMenu = MainMenu()
setDockIconAndMainMenuVisibility(isVisible: !isLaunchedAtLogin)
TunnelsManager.create { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: nil)
case .success(let tunnelsManager):
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
statusMenu.windowDelegate = self
let statusItemController = StatusItemController()
statusItemController.statusItem.menu = statusMenu
let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
tunnelsTracker.statusMenu = statusMenu
tunnelsTracker.statusItemController = statusItemController
self.tunnelsManager = tunnelsManager
self.tunnelsTracker = tunnelsTracker
self.statusItemController = statusItemController
if !isLaunchedAtLogin {
self.showManageTunnelsWindow(completion: nil)
}
}
}
}
@objc func confirmAndQuit() {
let alert = NSAlert()
alert.messageText = tr("macConfirmAndQuitAlertMessage")
if let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating {
alert.informativeText = tr(format: "macConfirmAndQuitInfoWithActiveTunnel (%@)", currentTunnel.name)
} else {
alert.informativeText = tr("macConfirmAndQuitAlertInfo")
}
alert.addButton(withTitle: tr("macConfirmAndQuitAlertCloseWindow"))
alert.addButton(withTitle: tr("macConfirmAndQuitAlertQuitWireGuard"))
NSApp.activate(ignoringOtherApps: true)
if let manageWindow = manageTunnelsWindowObject {
manageWindow.orderFront(self)
alert.beginSheetModal(for: manageWindow) { response in
switch response {
case .alertFirstButtonReturn:
manageWindow.close()
case .alertSecondButtonReturn:
NSApp.terminate(nil)
default:
break
}
}
}
}
@objc func quit() {
if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
NSApp.activate(ignoringOtherApps: true)
manageWindow.orderFront(self)
return
}
registerLoginItem(shouldLaunchAtLogin: false)
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
NSApp.terminate(nil)
return
}
let alert = NSAlert()
alert.messageText = tr("macAppExitingWithActiveTunnelMessage")
alert.informativeText = tr("macAppExitingWithActiveTunnelInfo")
NSApp.activate(ignoringOtherApps: true)
if let manageWindow = manageTunnelsWindowObject {
manageWindow.orderFront(self)
alert.beginSheetModal(for: manageWindow) { _ in
NSApp.terminate(nil)
}
} else {
alert.runModal()
NSApp.terminate(nil)
}
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
return .terminateNow
}
guard let appleEvent = NSAppleEventManager.shared().currentAppleEvent else {
return .terminateNow
}
guard MacAppStoreUpdateDetector.isUpdatingFromMacAppStore(quitAppleEvent: appleEvent) else {
return .terminateNow
}
let alert = NSAlert()
alert.messageText = tr("macAppStoreUpdatingAlertMessage")
if currentTunnel.isActivateOnDemandEnabled {
alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithOnDemand (%@)", currentTunnel.name)
} else {
alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)", currentTunnel.name)
}
NSApp.activate(ignoringOtherApps: true)
if let manageWindow = manageTunnelsWindowObject {
alert.beginSheetModal(for: manageWindow) { _ in }
} else {
alert.runModal()
}
return .terminateCancel
}
func applicationShouldTerminateAfterLastWindowClosed(_ application: NSApplication) -> Bool {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
self?.setDockIconAndMainMenuVisibility(isVisible: false)
}
return false
}
private func setDockIconAndMainMenuVisibility(isVisible: Bool, completion: (() -> Void)? = nil) {
let currentActivationPolicy = NSApp.activationPolicy()
let newActivationPolicy: NSApplication.ActivationPolicy = isVisible ? .regular : .accessory
guard currentActivationPolicy != newActivationPolicy else {
if newActivationPolicy == .regular {
NSApp.activate(ignoringOtherApps: true)
}
completion?()
return
}
if newActivationPolicy == .regular && NSApp.isActive {
// To workaround a possible AppKit bug that causes the main menu to become unresponsive,
// we should deactivate the app first and then set the activation policy.
// NSApp.deactivate() doesn't always deactivate the app, so we instead use
// setActivationPolicy(.prohibited).
onAppDeactivation = {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
completion?()
}
NSApp.setActivationPolicy(.prohibited)
} else {
NSApp.setActivationPolicy(newActivationPolicy)
if newActivationPolicy == .regular {
NSApp.activate(ignoringOtherApps: true)
}
completion?()
}
}
func applicationDidResignActive(_ notification: Notification) {
onAppDeactivation?()
onAppDeactivation = nil
}
}
extension AppDelegate {
@objc func aboutClicked() {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
let appVersionString = [
tr(format: "macAppVersion (%@)", appVersion),
tr(format: "macGoBackendVersion (%@)", WIREGUARD_GO_VERSION)
].joined(separator: "\n")
NSApp.activate(ignoringOtherApps: true)
NSApp.orderFrontStandardAboutPanel(options: [
.applicationVersion: appVersionString,
.version: "",
.credits: ""
])
}
}
extension AppDelegate: StatusMenuWindowDelegate {
func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?) {
guard let tunnelsManager = tunnelsManager else {
completion?(nil)
return
}
if manageTunnelsWindowObject == nil {
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
let window = NSWindow(contentViewController: manageTunnelsRootVC!)
window.title = tr("macWindowTitleManageTunnels")
window.setContentSize(NSSize(width: 800, height: 480))
window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
manageTunnelsWindowObject = window
tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
}
setDockIconAndMainMenuVisibility(isVisible: true) { [weak manageTunnelsWindowObject] in
manageTunnelsWindowObject?.makeKeyAndOrderFront(self)
completion?(manageTunnelsWindowObject)
}
}
}
@discardableResult
func registerLoginItem(shouldLaunchAtLogin: Bool) -> Bool {
let appId = Bundle.main.bundleIdentifier!
let helperBundleId = "\(appId).login-item-helper"
return SMLoginItemSetEnabled(helperBundleId as CFString, shouldLaunchAtLogin)
}
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.
import Cocoa
class Application: NSApplication {
private var appDelegate: AppDelegate? // swiftlint:disable:this weak_delegate
override init() {
super.init()
appDelegate = AppDelegate() // Keep a strong reference to the app delegate
delegate = appDelegate // Set delegate before app.run() gets called in NSApplicationMain()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

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