Compare commits

..

152 Commits

Author SHA1 Message Date
Jason A. Donenfeld 1450538846 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 05:08:31 +01:00
Eric Kuck 5a08c67f33 Fixed editable KeyValueCells being copyable
Fixed DNS servers not saving

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 21:48:48 -06:00
Jason A. Donenfeld 1e9c806614 Fix confusing indentation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 04:42:46 +01:00
Eric Kuck ccd8cfe478 KeyValueCells now share code
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 20:02:37 -06:00
Eric Kuck cb051f695d Reorganized project structure
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:27:11 -06:00
Eric Kuck 7a24f18eb7 Most similar views now shared between ViewControllers
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:15:22 -06:00
Jason A. Donenfeld 83c95dc26d Prettier log time format
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 00:08:54 +01:00
Jason A. Donenfeld e0bc5e12b3 Simplify logging tags
This was roop's initial idea, and it turns out to be the better one, now
that we can pass cstrings more easily.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-14 22:53:42 +01:00
Roopesh Chander c4263da231 Fix tunnel remaining in 'Activating' state
It uses to remain in 'Activating' state when we don't get a status
update notification, for example, when turning on the tunnel repeatedly
without Internet connectivity.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 17:33:56 +05:30
Roopesh Chander 1eb3fd4de0 Fix status switch weird state after an error occurs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 16:50:36 +05:30
Jason A. Donenfeld 73be704b01 Deduplicate functions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-14 00:01:50 +01:00
Jason A. Donenfeld 2699c613bd Simplify filemanager extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:25:18 +01:00
Jason A. Donenfeld 74e983ea6f Can't -> cannot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:17:05 +01:00
Roopesh Chander 48552d2663 NE: Communicate last error to app through a shared file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 02:24:53 +05:30
Roopesh Chander 501e412b84 TunnelsManager: startActivation() need not take a tunnelConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 00:40:18 +05:30
Jason A. Donenfeld 77a26e4cd2 Localize swiftlint
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 20:06:37 +01:00
Eric Kuck 05d750539b Reorganized ViewControllers (split out UIViews and UITableViewCells into their own classes)
All swiftlint warnings except one fixed up

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-13 12:58:50 -06:00
Jason A. Donenfeld 7323a00612 Avoid escaping heap allocation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:43:12 +01:00
Jason A. Donenfeld a6912ca7a2 Tidy up str to gostr conversion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:36:51 +01:00
Roopesh Chander b256acc372 TunnelsManager: Remove mentions of 'internal error'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:53:17 +05:30
Roopesh Chander 7e093575a4 TunnelsManager: Ask to check Internet connectivity in error alert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:49:42 +05:30
Roopesh Chander 740ffd68b6 Remove unused code: InternetReachability
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:45:21 +05:30
Roopesh Chander f67e1d8fc4 TunnelsManager: Remove unused variable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:44:13 +05:30
Roopesh Chander 33af8845b6 TunnelsManager: Remove assert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:43:15 +05:30
Jason A. Donenfeld 154774ada2 Simplify C strings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:08:38 +01:00
Roopesh Chander 3bddab8a9e TunnelsManager: Fix race between multiple startActivation() calls
After startActivate() is called on a waiting tunnel, user might turn
on a different tunnel before the waiting tunnel's status gets updated.
This fix prevents that from happening.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:34:00 +05:30
Roopesh Chander f9239dae75 TunnelsManager: Reintroduce waiting for another tunnel to deactivate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:21:49 +05:30
Jason A. Donenfeld 642b627d27 Rewrite Logger
This reverts all of Roop's changes to the C code, and then rewrites the
logger logic to be cleaner.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 18:06:37 +01:00
Eric Kuck 38accad27d More reliable logo sizing
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-13 09:30:13 -06:00
Roopesh Chander bf58159d99 TunnelsManager: Report activation errors through the activationDelegate
Don't report activation errors through completion handlers

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:56:07 +05:30
Roopesh Chander efd4b28a0d Logging: Write versions from both app and extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:09:38 +05:30
Roopesh Chander ae565db371 Logging: file_log doesn't need the message type
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:06:57 +05:30
Roopesh Chander e199ed0d6c Logging: Tag the entries in the merged log
So we know which entry is from the app and which is from the network
extension.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 18:00:32 +05:30
Roopesh Chander ba1d0c05be Logging: Use ringlogger for logging from the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:20 +05:30
Roopesh Chander 12503ae51d Logging: ringlogger.c: Trim trailing newlines
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:14 +05:30
Roopesh Chander ae7fb7323f Logging: Use ringlogger for logging from the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:14 +05:30
Roopesh Chander 5ae9eec555 Avoid using 'VPN' in code where possible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:20:10 +05:30
Roopesh Chander 6528a581de mv WireGuard/WireGuard/VPN/ WireGuard/WireGuard/Tunnel/
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:14:21 +05:30
Jason A. Donenfeld e11224f394 Commit untested ringlogger code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 06:14:24 +01:00
Jason A. Donenfeld 5971c197bd Remove useless whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:26:04 +01:00
Jason A. Donenfeld ecbab37e0e Settings: better padding calculation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:22:13 +01:00
Eric Kuck 4eec53d6d3 Fixed hacky logo display for settings
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:16:28 -06:00
Eric Kuck 8a916beb38 More formatting nits and cyclomatic complexity fixes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:09:52 -06:00
Eric Kuck e4ac48bc75 More linter warnings fixed, enabled more swiftlint rules, project cleanup
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 15:33:14 -06:00
Eric Kuck d06cff2a36 Tons more swiftlint warnings fixed. Still a few remaining.
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 12:28:27 -06:00
Eric Kuck de14b76b4d Added swiftlint and fixed all errors (and a bunch, but not all, warnings)
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 11:40:57 -06:00
Roopesh Chander af78fa9a1c Zip importing: importFromFile should take a completionHandler
Deletion of the being-imported file should be done in the
completionHandler.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 19:24:18 +05:30
Roopesh Chander 7ef12d93a7 ErrorPresenter: Support onPresented for showErrorAlert(title:,message:)
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 19:17:06 +05:30
Roopesh Chander 8259145f85 Zip importing: Handle spaces in filenames correctly
Previously, if a filename of a .conf file inside the zip file
contained spaces, it was not imported.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 16:57:17 +05:30
Jason A. Donenfeld 034a1a12f7 Supply missing pieces of path change
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-12 01:11:43 +01:00
Eric Kuck 9bc7e58487 Fixed a potential race condition, better naming on PacketTunnelSettingsGenerator methods
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-11 16:59:15 -06:00
Eric Kuck 27265fc222 Added an (unfinished) NWPathMonitor implementation for reconnecting on network changes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-11 16:12:04 -06:00
Jason A. Donenfeld 63e67a5e13 Revert pure-go network monitoring and add wgSetConfig
This reverts commit 99f0e457c34480f25582d7b4ed509404712c648c and adds a
function too.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-11 22:25:54 +01:00
Roopesh Chander bde984625c State restoration: Don't create duplicate mainVC and tunnelsListVC
This creates a duplicate tunnels manager, leading to problems tracking
tunnel statuses.

To reproduce the bug that this commit fixes, you can do the following:
1. Remove all tunnels
2. Run in Xcode
3. Import zip with ~10 tunnels
4. Stop app in Xcode
5. Run in Xcode
6. Turn on 1st tunnel, after it's on, turn off
Turn on 2nd tunnel, after it's on, turn off
...
After 6-8 tunnels, the spinner doesn't show up, indicating that the
status is not being tracked.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 17:52:54 +05:30
Roopesh Chander 1ded24f0e0 TunnelsManager: Error out only on no-internet scenario
The other scenario happens even during reloading of a tunnel for activation.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 03:48:28 +05:30
Roopesh Chander 1fd0c56f08 Remove the feature of waiting for another tunnel to deactivate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 03:47:23 +05:30
Roopesh Chander e59dbe6364 TunnelsManager: Deactivate only when the status becomes 'connected'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:42:21 +05:30
Roopesh Chander 4d63a3e9bd Allow turning off the status switch of a waiting tunnel
It just means the waiting should be cancelled

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:01:54 +05:30
Roopesh Chander 9946d8f989 TunnelsManager: Handle status change in TunnelsManager
Rather than in TunnelContainer.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 02:01:49 +05:30
Roopesh Chander 15b6cf5412 Error handling: alertText() can be nil
Indicating that no alert is to be shown for that error.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-11 00:34:22 +05:30
Roopesh Chander 851bd8102d TunnelsManager: Don't act on status change on tunnelProviders we don't have
That causes errors we don't want, and duplicate notifications.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 17:03:23 +05:30
Roopesh Chander 0d7a585bf7 TunnelsManager: Always call the completion handler before returning
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 17:01:53 +05:30
Roopesh Chander 663bb02c68 TunnelsManager: Debugging helpers for tunnel status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:31:36 +05:30
Roopesh Chander b491b9c371 TunnelsManager: Handle deactivation of a waiting tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:28:41 +05:30
Roopesh Chander 707f292e4e Tunnels list: Fix AutoLayout error during deletion of a tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 12:59:31 +05:30
Roopesh Chander aa41bd7d6c Settings: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander ec5be76d06 Tunnel edit: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander 692b0c6519 Tunnel detail: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
Roopesh Chander bb836b8fdc Tunnels list: Dynamic Type support for the add button at the center
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 18:13:37 +05:30
Roopesh Chander 8eda4641ca Tunnels list: Dynamic Type support for the table view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 18:13:04 +05:30
Roopesh Chander 60e13ddbf6 Model: Declare keyLength constant and use that wherever applicable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-09 14:07:03 +05:30
Roopesh Chander 0dcb285b67 TunnelsManager: Observe status for all tunnels in one block
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 18:43:24 +05:30
Roopesh Chander 6838dc3a74 TunnelsManager: Remove unused variables
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 15:00:26 +05:30
Roopesh Chander 46f4be6087 Zip: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 13:43:00 +05:30
Jason A. Donenfeld 2fa2f58aad Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 23:58:53 +01:00
Jason A. Donenfeld 1eddb2c86d PacketTunnelProvider: Show log timestamp
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 23:56:26 +01:00
Jason A. Donenfeld ddccd6da4c wireguard-go-bridge: account for network changes
Everytime the network changes, we need to recreate the UDP socket,
because the ephemeral listen port is tied to the old physical interface.
As well, we need to re-set the IP addresses for each endpoint, so that
they're passed to getaddrinfo and are then resolved using DNS46.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 21:50:19 +01:00
Roopesh Chander aa0b288436 Zip: Increase size of buffer used to read data from the archive
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:55 +05:30
Roopesh Chander 71568d1899 Settings: Export log: Perform file operations in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:55 +05:30
Roopesh Chander 06a783f50a On-Demand: TunnelViewModel: Make activate-on-demand methods static
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:23 +05:30
Roopesh Chander 465c22f769 On-Demand: Move detail text to TunnelViewModel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 02:17:15 +05:30
Jason A. Donenfeld b7e5638681 Plist: Handle crypto export
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 18:52:14 +01:00
Jason A. Donenfeld 66b4aedd08 Make strings consistent
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-07 18:52:14 +01:00
Roopesh Chander 105eca7adc State restoration: Restore tunnel detail view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 19:05:08 +05:30
Roopesh Chander e3801308cb Main VC: No need to refresh statuses if the tunnelsManager isn't initialized yet
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 15:52:47 +05:30
Roopesh Chander 05d0429c50 Tunnels list: Deselect rows correctly
Do it like UITableViewController.clearsSelectionOnViewWillAppear would.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:23 +05:30
Roopesh Chander fa9a4921a8 Settings: Exporting configs as zip should open document picker
Because:
- Exporting UI should be consistent with importing UI
- UIActivityVC takes a long time to open

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander c827a00307 Error handling: Use ErrorPresenter.showErrorAlert() instead of per-VC showErrorAlert() methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander dcfa9473e9 Error handling: Use WireGuardAppError and WireGuardResult throughout the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 782dd2ea4e Error handling: Introduce a WireGuardResult type to handle errors in callbacks across the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander c9267ba634 Error handling: Introduce a WireGuardAppError protocol to manage errors
The alert strings shall be located next to where the errors are declared.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 8d26a3c536 Error handling: Cleanup Tunnels Manager errors
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 7631844fbe Error presenter: Always handle the passed error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 046b540e53 Tunnel detail: Cell status switch should be toggled only after the alert presentation completes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander f6faffa4c1 Refactoring: Consolidate file deletion into a separate function
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander 290bd192a0 NE: Logging: Log file should begin with version numbers and tunnel name
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-07 12:36:19 +05:30
Roopesh Chander bf86731879 NE: Logging: Make it clear which calls to wg_log use String and which use StaticString
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-04 15:58:53 +05:30
Roopesh Chander 4e386e85e4 Settings: Add timestamp to exported log
And remove the exported log afterwards.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-04 14:12:53 +05:30
Roopesh Chander 046d1413ec Refactor out VPN-handling stuff from tunnels list VC to the main VC
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-03 18:51:51 +05:30
Roopesh Chander e1b258353c VPN: Error out when tunnel activation fails because there's no internet
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-03 18:51:42 +05:30
Roopesh Chander cdd132d7d0 Settings: Export log file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-02 13:26:53 +05:30
Roopesh Chander 679d63294d NE: Write log to file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-30 00:36:33 +05:30
Roopesh Chander d01d46fde8 Info.plist: Add app group id for accessing from both the app and the network extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-29 14:05:27 +05:30
Roopesh Chander a3bc306b6e Xcode: Add app groups capability
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-29 12:01:45 +05:30
Roopesh Chander 3e1772ccd0 It's 'WiFi', not 'Wifi'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-28 12:41:35 +05:30
Roopesh Chander b946cbc0f3 NE: All DNS queries must first go through the VPN's DNS servers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-23 13:04:55 +05:30
Roopesh Chander 8590a8804a Tunnel view model: Invalidate the configuration object when updating allowedIPs using the 'Exclude private IPs' switch
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-19 15:22:27 +05:30
Roopesh Chander a117e3ae3e Config file parser: Be case-insensitive to attribute keys in the config file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-19 12:37:54 +05:30
Roopesh Chander ef2012330f Config file parser: Fix typo
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-17 22:52:37 +05:30
Roopesh Chander 6bb25782c1 Exporting: Export to zip in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander 1dac181803 Exporting: Refactor out zip exporting into a separate class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander d556729705 Exporting: No need to check for duplicate names - we disallow it at creation time itself
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander b1ae13652c Importing: Import from zip in a background thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-15 13:39:56 +05:30
Roopesh Chander 203d6b4596 Importing: Refactor out zip importing into a separate class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-14 18:52:10 +05:30
Roopesh Chander 2676ee0169 Tunnels manager: After saving after activating on-demand, reload tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-14 13:02:53 +05:30
Roopesh Chander c60c29b93c Tunnels manager: Need to keep VPN-on-demand tunnels's status under observation
Because they can turn on automatically, even while the app is in the foreground.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander bdb2411ebc Tunnel detail: Show VPN-on-demand information
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander 923d039a78 Tunnels manager: Keep track of NETunnelProviderManager's isOnDemandEnabled property
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander abb0312d38 Tunnel edit: Update for VPN-on-demand changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander f1b8de7312 Tunnel view model: VPN-on-demand stuff shouldn't be part of the tunnel model
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander cc122d7463 Model, Tunnels manager: Rewrite the model for VPN-on-demand
The VPN-on-demand settings should not be part of the tunnel
configuration. Rather, the onDemandRules stored in the
tunnel provider configuration serve as the one place
where the VPN-on-demand settings are stored.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-12 19:24:13 +05:30
Roopesh Chander 39a067cb96 TunnelsManager: Support for on-demand rules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:31:38 +05:30
Roopesh Chander c5e8b05e8b Tunnel edit, Tunnel view model: UI for providing On-Demand activation options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:18:36 +05:30
Roopesh Chander 4b7094d652 Model: Add activationType to tunnel configuration
We make sure existing tunnel serializations can be deserialized correctly.

We also bump up the tunnelConfigurationVersion, because the tunnel
configuration contents have changed.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-11 01:17:36 +05:30
Roopesh Chander 0f03ffc920 Model: ActivityType enum to represent VPN-on-demand options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 19:17:24 +05:30
Roopesh Chander 1502bd42d3 Model: TunnelConfiguration: Add explicit conformance to Decodable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 19:01:34 +05:30
Roopesh Chander 290f83d5ef Model: Ensure that a TunnelConfiguration always has a valid array of peers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 17:02:30 +05:30
Roopesh Chander e8d68396ca VPN: When activating while another tunnel is active, deactivate the other tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 8e7bfb15ed TunnelsManager: startDeactivation() need not take a completion handler
Because the completion handler pattern doesn't fit in this case.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 95456ec956 VPN: There are no DNS errors to handle in the app now
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-10 16:44:28 +05:30
Roopesh Chander 7485474c4c NE: Minor refactoring to enable calling startTunnel() with a tunnelConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 22:29:52 +05:30
Roopesh Chander 1d63509b92 VPN: Refresh tunnel statuses when app gets to the foreground
Because the tunnel could've be activated from iOS Settings now

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:29:34 +05:30
Roopesh Chander f3e32ab737 Remove unused code
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:20:33 +05:30
Roopesh Chander 59b9a6e5d2 TunnelsManager: Ability to refresh connection statuses
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 19:19:32 +05:30
Roopesh Chander 3136fe0e2c NE: When there's an error starting the tunnel, show it to the user using displayMessage()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-09 17:07:42 +05:30
Roopesh Chander a1070d2b29 Remove unused file PacketTunnelOptionKey.swift
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 18:59:16 +05:30
Roopesh Chander 5ee4d392b5 Move logic to extension: Bring up the tunnel from the stored providerConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 18:59:16 +05:30
Roopesh Chander c17e4a27a2 DNSResolver: Simplify
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 8409b7e929 DNSResolver: Let's not cache DNS resolution results anymore
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 651ffa0c51 DNSResolver: DNS resolution can now happen synchronously
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander 4404bb2b7d Model: Endpoint.hostname()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:11 +05:30
Roopesh Chander e66cf5264a Move logic to extension: NETunnelProviderProtocol extension code should be shared
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 17:52:01 +05:30
Roopesh Chander af58bfcb00 Move logic to extension: Refactor PacketTunnelOptionsGenerator into a PacketTunnelSettingsGenerator
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:44:13 +05:30
Roopesh Chander 2f7e437202 Move logic to extension: Move DNSResolver to extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:09:45 +05:30
Roopesh Chander 62573e2ad7 Move logic to extension: .resolvingEndpointDomains is not longer a valid status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:08:57 +05:30
Roopesh Chander a5f9dc4821 Move logic to extension: DNS resolution no longer happens in the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 15:04:12 +05:30
Roopesh Chander 7827147bc8 Move logic to extension: Include shared model code when building the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:50:05 +05:30
Roopesh Chander a473dfe4f8 Model: Move InterfaceConfiguration.publicKey to Curve25519.swift
The code for public key calculation need not be shared with the extension

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:48:25 +05:30
Roopesh Chander fb6a7f6007 Move logic to extension: Move PacketTunnelOptionsGenerator to the extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:16:30 +05:30
Roopesh Chander f7b04b0b5d Move logic to extension: Invoke startTunnel() without any options
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 14:15:01 +05:30
Roopesh Chander c88c660b51 Move logic to extension: Move model files to Shared
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 13:56:50 +05:30
Roopesh Chander ec2d67ea00 Tunnel edit: While preparing for reuse, should make onValueBeingEdited nil as well
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-11-08 12:25:36 +05:30
67 changed files with 4358 additions and 2989 deletions
+3
View File
@@ -38,3 +38,6 @@ fastlane/screenshots
fastlane/test_output
Preview.html
output
# Wireguard specific
WireGuard/WireGuard/Config/Developer.xcconfig
-12
View File
@@ -1,12 +0,0 @@
disabled_rules:
- line_length
- trailing_comma
excluded:
- Pods
file_length:
warning: 500
type_name:
min_length: 2 # only warning
max_length: # warning and error
warning: 50
error: 60
+6
View File
@@ -23,6 +23,12 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
```
- Install swiftlint:
```
$ brew install swiftlint
```
- Open project in XCode:
```
+14
View File
@@ -0,0 +1,14 @@
disabled_rules:
- line_length
- trailing_whitespace
- todo
opt_in_rules:
- unneeded_parentheses_in_closure_argument
# - trailing_closure
file_length:
warning: 500
cyclomatic_complexity:
warning: 10
error: 25
function_body_length:
warning: 45
@@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import os.log
extension FileManager {
private static var sharedFolderURL: URL? {
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
os_log("Cannot obtain app group ID from bundle", log: OSLog.default, type: .error)
return nil
}
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
wg_log(.error, message: "Cannot obtain shared folder URL")
return nil
}
return sharedFolderURL
}
static var networkExtensionLogFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
}
static var networkExtensionLastErrorFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("last-error.txt")
}
static var appLogFileURL: URL? {
guard let documentDirURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
wg_log(.error, message: "Cannot obtain app documents folder URL")
return nil
}
return documentDirURL.appendingPathComponent("app-log.bin")
}
static func deleteFile(at url: URL) -> Bool {
do {
try FileManager.default.removeItem(at: url)
} catch {
return false
}
return true
}
}
+67
View File
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import os.log
public class Logger {
enum LoggerError: Error {
case openFailure
}
static var global: Logger?
var log: OpaquePointer
init(withFilePath filePath: String) throws {
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
self.log = log
}
deinit {
close_log(self.log)
}
func log(message: String) {
write_msg_to_log(log, message.trimmingCharacters(in: .newlines))
}
func writeLog(called ourTag: String, mergedWith otherLogFile: String, called otherTag: String, to targetFile: String) -> Bool {
guard let other = open_log(otherLogFile) else { return false }
let ret = write_logs_to_file(targetFile, log, ourTag, other, otherTag)
close_log(other)
return ret == 0
}
static func configureGlobal(withFilePath filePath: String?) {
if Logger.global != nil {
return
}
guard let filePath = filePath else {
os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error)
return
}
do {
try Logger.global = Logger(withFilePath: filePath)
} catch {
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
return
}
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
let goBackendVersion = WIREGUARD_GO_VERSION
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
}
}
func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
os_log(msg, log: OSLog.default, type: type)
Logger.global?.log(message: "\(msg)")
}
func wg_log(_ type: OSLogType, message msg: String) {
os_log("%{public}s", log: OSLog.default, type: type, msg)
Logger.global?.log(message: msg)
}
+147
View File
@@ -0,0 +1,147 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
*/
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/mman.h>
#include "ringlogger.h"
enum {
MAX_LOG_LINE_LENGTH = 512,
MAX_LINES = 1024,
MAGIC = 0xbeefbabeU
};
struct log_line {
struct timeval tv;
char line[MAX_LOG_LINE_LENGTH];
};
struct log {
struct { uint32_t first, len; } header;
struct log_line lines[MAX_LINES];
uint32_t magic;
};
void write_msg_to_log(struct log *log, const char *msg)
{
struct log_line *line = &log->lines[(log->header.first + log->header.len) % MAX_LINES];
if (log->header.len == MAX_LINES)
log->header.first = (log->header.first + 1) % MAX_LINES;
else
++log->header.len;
gettimeofday(&line->tv, NULL);
strncpy(line->line, msg, MAX_LOG_LINE_LENGTH - 1);
line->line[MAX_LOG_LINE_LENGTH - 1] = '\0';
msync(&log->header, sizeof(log->header), MS_ASYNC);
msync(line, sizeof(*line), MS_ASYNC);
}
static bool first_before_second(const struct log_line *line1, const struct log_line *line2)
{
if (line1->tv.tv_sec <= line2->tv.tv_sec)
return true;
if (line1->tv.tv_sec == line2->tv.tv_sec)
return line1->tv.tv_usec <= line2->tv.tv_usec;
return false;
}
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2)
{
uint32_t i1, i2, len1 = log1->header.len, len2 = log2->header.len;
FILE *file;
int ret;
if (len1 > MAX_LINES)
len1 = MAX_LINES;
if (len2 > MAX_LINES)
len2 = MAX_LINES;
file = fopen(file_name, "w");
if (!file)
return -errno;
for (i1 = 0, i2 = 0;;) {
struct tm tm;
char buf[MAX_LOG_LINE_LENGTH];
const struct log_line *line1 = &log1->lines[(log1->header.first + i1) % MAX_LINES];
const struct log_line *line2 = &log2->lines[(log2->header.first + i2) % MAX_LINES];
const struct log_line *line;
const char *tag;
if (i1 < len1 && (i2 >= len2 || first_before_second(line1, line2))) {
line = line1;
tag = tag1;
++i1;
} else if (i2 < len2 && (i1 >= len1 || first_before_second(line2, line1))) {
line = line2;
tag = tag2;
++i2;
} else {
break;
}
memcpy(buf, line->line, MAX_LOG_LINE_LENGTH);
buf[MAX_LOG_LINE_LENGTH - 1] = '\0';
if (!localtime_r(&line->tv.tv_sec, &tm))
goto err;
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: [%s] %s\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, line->tv.tv_usec,
tag, buf) < 0)
goto err;
}
errno = 0;
err:
ret = -errno;
fclose(file);
return ret;
}
struct log *open_log(const char *file_name)
{
int fd;
struct log *log;
fd = open(file_name, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd < 0)
return NULL;
if (ftruncate(fd, sizeof(*log)))
goto err;
log = mmap(NULL, sizeof(*log), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (log == MAP_FAILED)
goto err;
close(fd);
if (log->magic != MAGIC) {
memset(log, 0, sizeof(*log));
log->magic = MAGIC;
msync(log, sizeof(*log), MS_ASYNC);
}
return log;
err:
close(fd);
return NULL;
}
void close_log(struct log *log)
{
munmap(log, sizeof(*log));
}
+15
View File
@@ -0,0 +1,15 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018 WireGuard LLC. All Rights Reserved.
*/
#ifndef RINGLOGGER_H
#define RINGLOGGER_H
struct log;
void write_msg_to_log(struct log *log, const char *msg);
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2);
struct log *open_log(const char *file_name);
void close_log(struct log *log);
#endif
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
@available(OSX 10.14, iOS 12.0, *)
final class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength: Int = 32
init(interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
let peerPublicKeysArray = peers.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
fatalError("Two or more peers cannot have the same public key")
}
}
}
@available(OSX 10.14, iOS 12.0, *)
struct InterfaceConfiguration: Codable {
var name: String
var privateKey: Data
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns = [DNSServer]()
init(name: String, privateKey: Data) {
self.name = name
self.privateKey = privateKey
if name.isEmpty {
fatalError("Empty name")
}
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
}
}
@available(OSX 10.14, iOS 12.0, *)
struct PeerConfiguration: Codable {
var publicKey: Data
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if value.count != TunnelConfiguration.keyLength {
fatalError("Invalid preshared key")
}
}
}
}
var allowedIPs = [IPAddressRange]()
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
init(publicKey: Data) {
self.publicKey = publicKey
if publicKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid public key")
}
}
}
@@ -40,7 +40,7 @@ extension DNSServer: Codable {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch (data.count) {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
@@ -17,15 +17,15 @@ extension Endpoint {
init?(from string: String) {
// Separation of host and port is based on 'parse_endpoint' function in
// https://git.zx2c4.com/WireGuard/tree/src/tools/config.c
guard (!string.isEmpty) else { return nil }
guard !string.isEmpty else { return nil }
let startOfPort: String.Index
let hostString: String
if (string.first! == "[") {
if string.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = string.index(after: string.startIndex)
guard let endOfHost = string.dropFirst().firstIndex(of: "]") else { return nil }
let afterEndOfHost = string.index(after: endOfHost)
guard (string[afterEndOfHost] == ":") else { return nil }
guard string[afterEndOfHost] == ":" else { return nil }
startOfPort = string.index(after: afterEndOfHost)
hostString = String(string[startOfHost ..< endOfHost])
} else {
@@ -35,15 +35,15 @@ extension Endpoint {
hostString = String(string[string.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(string[startOfPort ..< string.endIndex])) else { return nil }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { (c) -> Bool in
return !CharacterSet.urlHostAllowed.contains(c)
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard (invalidCharacterIndex == nil) else { return nil }
guard invalidCharacterIndex == nil else { return nil }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
func stringRepresentation() -> String {
switch (host) {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
@@ -61,7 +61,7 @@ extension Endpoint {
extension Endpoint: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.stringRepresentation())
try container.encode(stringRepresentation())
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -78,13 +78,24 @@ extension Endpoint: Codable {
extension Endpoint {
func hasHostAsIPAddress() -> Bool {
switch (host) {
case .name(_, _):
switch host {
case .name:
return false
case .ipv4(_):
case .ipv4:
return true
case .ipv6(_):
case .ipv6:
return true
}
}
func hostname() -> String? {
switch host {
case .name(let hostname, _):
return hostname
case .ipv4:
return nil
case .ipv6:
return nil
}
}
}
@@ -27,9 +27,9 @@ extension IPAddressRange {
}
let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128
var networkPrefixLength: UInt8
if (endOfIPAddress < string.endIndex) { // "/" was located
if endOfIPAddress < string.endIndex { // "/" was located
let indexOfNetworkPrefixLength = string.index(after: endOfIPAddress)
guard (indexOfNetworkPrefixLength < string.endIndex) else { return nil }
guard indexOfNetworkPrefixLength < string.endIndex else { return nil }
let networkPrefixLengthSubstring = string[indexOfNetworkPrefixLength ..< string.endIndex]
guard let npl = UInt8(networkPrefixLengthSubstring) else { return nil }
networkPrefixLength = min(npl, maxNetworkPrefixLength)
@@ -69,15 +69,13 @@ extension IPAddressRange: Codable {
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch (data.count) {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
address = ipAddress
}
enum DecodingError: Error {
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
extension NETunnelProviderProtocol {
convenience init?(tunnelConfiguration: TunnelConfiguration) {
assert(!tunnelConfiguration.interface.name.isEmpty)
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
self.init()
let appId = Bundle.main.bundleIdentifier!
providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [
"tunnelConfiguration": serializedTunnelConfiguration,
"tunnelConfigurationVersion": 1
]
let endpoints = tunnelConfiguration.peers.compactMap {$0.endpoint}
if endpoints.count == 1 {
serverAddress = endpoints.first!.stringRepresentation()
} else if endpoints.isEmpty {
serverAddress = "Unspecified"
} else {
serverAddress = "Multiple endpoints"
}
username = tunnelConfiguration.interface.name
}
func tunnelConfiguration() -> TunnelConfiguration? {
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
}
}
@@ -1,30 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
enum PacketTunnelOptionKey: String {
case interfaceName, wireguardSettings, remoteAddress, dnsServers, mtu,
// IPv4 settings
ipv4Addresses, ipv4SubnetMasks,
ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks,
ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks,
// IPv6 settings
ipv6Addresses, ipv6NetworkPrefixLengths,
ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths,
ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths
}
extension Dictionary where Key == String {
subscript(key: PacketTunnelOptionKey) -> Value? {
get {
return self[key.rawValue]
}
set(value) {
self[key.rawValue] = value
}
}
}
+196 -31
View File
@@ -7,14 +7,21 @@
objects = {
/* Begin PBXBuildFile section */
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */; };
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0068562191AFD200419BE9 /* ScrollableLabel.swift */; };
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */; };
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */; };
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45418F21C2D53800994C13 /* SwitchCell.swift */; };
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419121C2D55800994C13 /* CheckmarkCell.swift */; };
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419721C2D60500994C13 /* KeyValueCell.swift */; };
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F45419F21C2D6B700994C13 /* TunnelListCell.swift */; };
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */; };
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A521C4449E00994C13 /* ButtonCell.swift */; };
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */; };
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */; };
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
@@ -42,8 +49,24 @@
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; };
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; };
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; };
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FE219C60290028284D /* ZipExporter.swift */; };
6FF3527021C240160008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
6FF3527121C240160008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
6FF3527221C2616C0008484E /* ringlogger.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526C21C23F960008484E /* ringlogger.c */; };
6FF3527321C2616C0008484E /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF3526E21C23FA10008484E /* Logger.swift */; };
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; };
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; };
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1421832391000F85AD /* DNSResolver.swift */; };
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -78,16 +101,25 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CopyableLabelTableViewCell.swift; sourceTree = "<group>"; };
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollableLabel.swift; sourceTree = "<group>"; };
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+Reuse.swift"; sourceTree = "<group>"; };
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditKeyValueCell.swift; sourceTree = "<group>"; };
5F45418F21C2D53800994C13 /* SwitchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
5F45419121C2D55800994C13 /* CheckmarkCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCell.swift; sourceTree = "<group>"; };
5F45419721C2D60500994C13 /* KeyValueCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueCell.swift; sourceTree = "<group>"; };
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListCell.swift; sourceTree = "<group>"; };
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BorderedTextButton.swift; sourceTree = "<group>"; };
5F4541A521C4449E00994C13 /* ButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonCell.swift; sourceTree = "<group>"; };
5F4541A821C451D100994C13 /* TunnelStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelStatus.swift; sourceTree = "<group>"; };
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Extension.swift"; sourceTree = "<group>"; };
6F5D0C1421832391000F85AD /* DNSResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WireGuardNetworkExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
6F5D0C1E218352EF000F85AD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension.entitlements; sourceTree = "<group>"; };
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionKey.swift; sourceTree = "<group>"; };
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelOptionsGenerator.swift; sourceTree = "<group>"; };
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; };
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; };
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; };
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
@@ -120,6 +152,11 @@
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = "<group>"; };
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = "<group>"; };
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = "<group>"; };
6FE254FA219C10800028284D /* ZipImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipImporter.swift; sourceTree = "<group>"; };
6FE254FE219C60290028284D /* ZipExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipExporter.swift; sourceTree = "<group>"; };
6FF3526B21C23F960008484E /* ringlogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ringlogger.h; sourceTree = "<group>"; };
6FF3526C21C23F960008484E /* ringlogger.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ringlogger.c; sourceTree = "<group>"; };
6FF3526E21C23FA10008484E /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -127,6 +164,9 @@
6FF4AC2B211EC776002C96EB /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Config.xcconfig; path = Config/Config.xcconfig; sourceTree = "<group>"; };
6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -148,13 +188,43 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
5F4541A721C44F5B00994C13 /* View */ = {
isa = PBXGroup;
children = (
5F45419F21C2D6B700994C13 /* TunnelListCell.swift */,
5F45418B21C2D48200994C13 /* TunnelEditKeyValueCell.swift */,
5F45418F21C2D53800994C13 /* SwitchCell.swift */,
5F45419721C2D60500994C13 /* KeyValueCell.swift */,
5F4541A521C4449E00994C13 /* ButtonCell.swift */,
5F45419121C2D55800994C13 /* CheckmarkCell.swift */,
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
);
path = View;
sourceTree = "<group>";
};
5F4541AC21C4720B00994C13 /* ViewController */ = {
isa = PBXGroup;
children = (
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
);
path = ViewController;
sourceTree = "<group>";
};
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */ = {
isa = PBXGroup;
children = (
6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */,
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */,
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
6F5D0C1E218352EF000F85AD /* Info.plist */,
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension.entitlements */,
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */,
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */,
);
path = WireGuardNetworkExtension;
sourceTree = "<group>";
@@ -162,7 +232,10 @@
6F5D0C432183B4A4000F85AD /* Shared */ = {
isa = PBXGroup;
children = (
6F5D0C442183BCDA000F85AD /* PacketTunnelOptionKey.swift */,
6FF3526A21C23F720008484E /* Logging */,
6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
);
path = Shared;
sourceTree = "<group>";
@@ -198,16 +271,11 @@
6F7774DE217181B1006A79B3 /* iOS */ = {
isa = PBXGroup;
children = (
6BB8400321892C920003598F /* CopyableLabelTableViewCell.swift */,
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */,
5F4541AC21C4720B00994C13 /* ViewController */,
5F4541A721C44F5B00994C13 /* View */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */,
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */,
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */,
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
6F0068562191AFD200419BE9 /* ScrollableLabel.swift */,
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
);
path = iOS;
sourceTree = "<group>";
@@ -223,14 +291,14 @@
path = Model;
sourceTree = "<group>";
};
6F7774ED21722D0C006A79B3 /* VPN */ = {
6F7774ED21722D0C006A79B3 /* Tunnel */ = {
isa = PBXGroup;
children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6F5D0C1421832391000F85AD /* DNSResolver.swift */,
6F5D0C472183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
5F4541A821C451D100994C13 /* TunnelStatus.swift */,
);
path = VPN;
path = Tunnel;
sourceTree = "<group>";
};
6F919ED3218C65C50023B400 /* Resources */ = {
@@ -255,6 +323,8 @@
6FDEF7E72186320E00D8FBF6 /* ZipArchive */ = {
isa = PBXGroup;
children = (
6FE254FA219C10800028284D /* ZipImporter.swift */,
6FE254FE219C60290028284D /* ZipExporter.swift */,
6FDEF801218646B900D8FBF6 /* ZipArchive.swift */,
6FDEF7F421863B6100D8FBF6 /* 3rdparty */,
);
@@ -282,6 +352,16 @@
path = minizip;
sourceTree = "<group>";
};
6FF3526A21C23F720008484E /* Logging */ = {
isa = PBXGroup;
children = (
6FF3526C21C23F960008484E /* ringlogger.c */,
6FF3526B21C23F960008484E /* ringlogger.h */,
6FF3526E21C23FA10008484E /* Logger.swift */,
);
path = Logging;
sourceTree = "<group>";
};
6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup;
children = (
@@ -308,10 +388,11 @@
6F919ED3218C65C50023B400 /* Resources */,
6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */,
6F7774E6217201E0006A79B3 /* Model */,
6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* VPN */,
6F7774ED21722D0C006A79B3 /* Tunnel */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
@@ -355,6 +436,8 @@
isa = PBXNativeTarget;
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
buildPhases = (
5F45417B21C0906F00994C13 /* Swiftlint */,
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
6F5D0C16218352EF000F85AD /* Sources */,
6F5D0C17218352EF000F85AD /* Frameworks */,
6F5D0C18218352EF000F85AD /* Resources */,
@@ -373,6 +456,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
buildPhases = (
5F45417A21C0902400994C13 /* Swiftlint */,
6B87860E2189532500C099FB /* Extract wireguard-go Version */,
6FF4AC10211EC46F002C96EB /* Sources */,
6FF4AC11211EC46F002C96EB /* Frameworks */,
@@ -410,6 +494,9 @@
CreatedOnToolsVersion = 9.4.1;
LastSwiftMigration = 1000;
SystemCapabilities = {
com.apple.ApplicationGroups.iOS = {
enabled = 1;
};
com.apple.NetworkExtensions.iOS = {
enabled = 1;
};
@@ -461,6 +548,42 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
5F45417A21C0902400994C13 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
5F45417B21C0906F00994C13 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -480,6 +603,25 @@
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
showEnvVarsInLog = 0;
};
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Extract wireguard-go Version";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "exec make -C \"$PROJECT_DIR/../wireguard-go-bridge\" version-header\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -487,7 +629,17 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6F5D0C462183C0B4000F85AD /* PacketTunnelOptionKey.swift in Sources */,
6FF3527021C240160008484E /* ringlogger.c in Sources */,
6FF3527121C240160008484E /* Logger.swift in Sources */,
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */,
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -496,33 +648,46 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
6F5D0C1521832391000F85AD /* DNSResolver.swift in Sources */,
6F5D0C482183C6A3000F85AD /* PacketTunnelOptionsGenerator.swift in Sources */,
6BB8400421892C920003598F /* CopyableLabelTableViewCell.swift in Sources */,
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6F0068572191AFD200419BE9 /* ScrollableLabel.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F5D0C452183BCDA000F85AD /* PacketTunnelOptionKey.swift in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */,
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */,
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */,
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */,
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */,
6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */,
5F45419021C2D53800994C13 /* SwitchCell.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+1 -1
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104
VERSION_ID = 4
VERSION_ID = 6
@@ -20,83 +20,17 @@ class WgQuickConfigFileParser {
case invalidPeer
}
//swiftlint:disable:next cyclomatic_complexity function_body_length
static func parse(_ text: String, name: String) throws -> TunnelConfiguration {
assert(!name.isEmpty)
func collate(interfaceAttributes attributes: [String: String]) -> InterfaceConfiguration? {
// required wg fields
guard let privateKeyString = attributes["PrivateKey"] else { return nil }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
// other wg fields
if let listenPortString = attributes["ListenPort"] {
guard let listenPort = UInt16(listenPortString) else { return nil }
interface.listenPort = listenPort
}
// wg-quick fields
if let addressesString = attributes["Address"] {
var addresses: [IPAddressRange] = []
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
guard let address = IPAddressRange(from: trimmedString) else { return nil }
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["DNS"] {
var dnsServers: [DNSServer] = []
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
dnsServers.append(dnsServer)
}
interface.dns = dnsServers
}
if let mtuString = attributes["MTU"] {
guard let mtu = UInt16(mtuString) else { return nil }
interface.mtu = mtu
}
return interface
}
func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields
guard let publicKeyString = attributes["PublicKey"] else { return nil }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else { return nil }
var peer = PeerConfiguration(publicKey: publicKey)
// wg fields
if let preSharedKeyString = attributes["PreSharedKey"] {
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 else { return nil }
peer.preSharedKey = preSharedKey
}
if let allowedIPsString = attributes["AllowedIPs"] {
var allowedIPs: [IPAddressRange] = []
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["Endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else { return nil }
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["PersistentKeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations: [PeerConfiguration] = []
var peerConfigurations = [PeerConfiguration]()
let lines = text.split(separator: "\n")
var parserState: ParserState = .notInASection
var attributes: [String: String] = [:]
var parserState = ParserState.notInASection
var attributes = [String: String]()
for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String
@@ -113,38 +47,36 @@ class WgQuickConfigFileParser {
if let equalsIndex = line.firstIndex(of: "=") {
// Line contains an attribute
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces)
let key = line[..<equalsIndex].trimmingCharacters(in: .whitespaces).lowercased()
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
let keysWithMultipleEntriesAllowed: Set<String> = ["Address", "AllowedIPs", "DNS"]
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
if let presentValue = attributes[key], keysWithMultipleEntriesAllowed.contains(key) {
attributes[key] = presentValue + "," + value
} else {
attributes[key] = value
}
} else {
if (lowercasedLine != "[interface]" && lowercasedLine != "[peer]") {
throw ParseError.invalidLine(line)
}
} else if lowercasedLine != "[interface]" && lowercasedLine != "[peer]" {
throw ParseError.invalidLine(line)
}
let isLastLine: Bool = (lineIndex == lines.count - 1)
let isLastLine = lineIndex == lines.count - 1
if (isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]") {
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
// Previous section has ended; process the attributes collected so far
if (parserState == .inInterfaceSection) {
guard let interface = collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
guard (interfaceConfiguration == nil) else { throw ParseError.multipleInterfaces }
if parserState == .inInterfaceSection {
guard let interface = collate(interfaceAttributes: attributes, name: name) else { throw ParseError.invalidInterface }
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
} else if (parserState == .inPeerSection) {
} else if parserState == .inPeerSection {
guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer)
}
}
if (lowercasedLine == "[interface]") {
if lowercasedLine == "[interface]" {
parserState = .inInterfaceSection
attributes.removeAll()
} else if (lowercasedLine == "[peer]") {
} else if lowercasedLine == "[peer]" {
parserState = .inPeerSection
attributes.removeAll()
}
@@ -152,16 +84,84 @@ class WgQuickConfigFileParser {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey
}
if let interfaceConfiguration = interfaceConfiguration {
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
tunnelConfiguration.peers = peerConfigurations
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
return tunnelConfiguration
} else {
throw ParseError.noInterface
}
}
//swiftlint:disable:next cyclomatic_complexity
private static func collate(interfaceAttributes attributes: [String: String], name: String) -> InterfaceConfiguration? {
// required wg fields
guard let privateKeyString = attributes["privatekey"] else { return nil }
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else { return nil }
var interface = InterfaceConfiguration(name: name, privateKey: privateKey)
// other wg fields
if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else { return nil }
interface.listenPort = listenPort
}
// wg-quick fields
if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: .whitespaces)
guard let address = IPAddressRange(from: trimmedString) else { return nil }
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: .whitespaces)
guard let dnsServer = DNSServer(from: trimmedString) else { return nil }
dnsServers.append(dnsServer)
}
interface.dns = dnsServers
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else { return nil }
interface.mtu = mtu
}
return interface
}
//swiftlint:disable:next cyclomatic_complexity
private static func collate(peerAttributes attributes: [String: String]) -> PeerConfiguration? {
// required wg fields
guard let publicKeyString = attributes["publickey"] else { return nil }
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else { return nil }
var peer = PeerConfiguration(publicKey: publicKey)
// wg fields
if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else { return nil }
peer.preSharedKey = preSharedKey
}
if let allowedIPsString = attributes["allowedips"] {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard let allowedIP = IPAddressRange(from: trimmedString) else { return nil }
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else { return nil }
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else { return nil }
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
}
@@ -4,18 +4,18 @@
import UIKit
class WgQuickConfigFileWriter {
static func writeConfigFile(from tc: TunnelConfiguration) -> Data? {
let interface = tc.interface
static func writeConfigFile(from configuration: TunnelConfiguration) -> Data? {
let interface = configuration.interface
var output = "[Interface]\n"
output.append("PrivateKey = \(interface.privateKey.base64EncodedString())\n")
if let listenPort = interface.listenPort {
output.append("ListenPort = \(listenPort)\n")
}
if (!interface.addresses.isEmpty) {
if !interface.addresses.isEmpty {
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if (!interface.dns.isEmpty) {
if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
@@ -23,13 +23,13 @@ class WgQuickConfigFileWriter {
output.append("MTU = \(mtu)\n")
}
for peer in tc.peers {
for peer in configuration.peers {
output.append("\n[Peer]\n")
output.append("PublicKey = \(peer.publicKey.base64EncodedString())\n")
if let preSharedKey = peer.preSharedKey {
output.append("PresharedKey = \(preSharedKey.base64EncodedString())\n")
}
if (!peer.allowedIPs.isEmpty) {
if !peer.allowedIPs.isEmpty {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
+17 -8
View File
@@ -4,24 +4,33 @@
import UIKit
struct Curve25519 {
static let keyLength: Int = 32
static func generatePrivateKey() -> Data {
var privateKey = Data(repeating: 0, count: 32)
privateKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeMutableBytes { bytes in
curve25519_generate_private_key(bytes)
}
assert(privateKey.count == 32)
assert(privateKey.count == TunnelConfiguration.keyLength)
return privateKey
}
static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data {
assert(privateKey.count == 32)
var publicKey = Data(repeating: 0, count: 32)
privateKey.withUnsafeBytes { (privateKeyBytes: UnsafePointer<UInt8>) in
publicKey.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer<UInt8>) in
assert(privateKey.count == TunnelConfiguration.keyLength)
var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeBytes { privateKeyBytes in
publicKey.withUnsafeMutableBytes { bytes in
curve25519_derive_public_key(bytes, privateKeyBytes)
}
}
assert(publicKey.count == 32)
assert(publicKey.count == TunnelConfiguration.keyLength)
return publicKey
}
}
extension InterfaceConfiguration {
var publicKey: Data {
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
}
}
+4
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
@@ -120,5 +122,7 @@
</dict>
</dict>
</array>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID)</string>
</dict>
</plist>
@@ -1,54 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
@available(OSX 10.14, iOS 12.0, *)
class TunnelConfiguration: Codable {
var interface: InterfaceConfiguration
var peers: [PeerConfiguration] = []
init(interface: InterfaceConfiguration) {
self.interface = interface
}
}
@available(OSX 10.14, iOS 12.0, *)
struct InterfaceConfiguration: Codable {
var name: String
var privateKey: Data
var addresses: [IPAddressRange] = []
var listenPort: UInt16?
var mtu: UInt16?
var dns: [DNSServer] = []
var publicKey: Data {
return Curve25519.generatePublicKey(fromPrivateKey: privateKey)
}
init(name: String, privateKey: Data) {
self.name = name
self.privateKey = privateKey
if (name.isEmpty) { fatalError("Empty name") }
if (privateKey.count != 32) { fatalError("Invalid private key") }
}
}
@available(OSX 10.14, iOS 12.0, *)
struct PeerConfiguration: Codable {
var publicKey: Data
var preSharedKey: Data? {
didSet(value) {
if let value = value {
if (value.count != 32) { fatalError("Invalid preshared key") }
}
}
}
var allowedIPs: [IPAddressRange] = []
var endpoint: Endpoint?
var persistentKeepAlive: UInt16?
init(publicKey: Data) {
self.publicKey = publicKey
if (publicKey.count != 32) { fatalError("Invalid public key") }
}
}
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
struct ActivateOnDemandSetting {
var isActivateOnDemandEnabled: Bool
var activateOnDemandOption: ActivateOnDemandOption
}
enum ActivateOnDemandOption {
case none // Valid only when isActivateOnDemandEnabled is false
case useOnDemandOverWiFiOrCellular
case useOnDemandOverWiFiOnly
case useOnDemandOverCellularOnly
}
extension ActivateOnDemandSetting {
func apply(on tunnelProviderManager: NETunnelProviderManager) {
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect()
switch activateOnDemandOption {
case .none:
rules = nil
case .useOnDemandOverWiFiOrCellular:
rules = [connectRule]
case .useOnDemandOverWiFiOnly:
connectRule.interfaceTypeMatch = .wiFi
disconnectRule.interfaceTypeMatch = .cellular
rules = [connectRule, disconnectRule]
case .useOnDemandOverCellularOnly:
connectRule.interfaceTypeMatch = .cellular
disconnectRule.interfaceTypeMatch = .wiFi
rules = [connectRule, disconnectRule]
}
tunnelProviderManager.onDemandRules = rules
}
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption
switch rules.count {
case 0:
activateOnDemandOption = .none
case 1:
let rule = rules[0]
precondition(rule.action == .connect)
activateOnDemandOption = .useOnDemandOverWiFiOrCellular
case 2:
let connectRule = rules.first(where: { $0.action == .connect })!
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
activateOnDemandOption = .useOnDemandOverWiFiOnly
} else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .useOnDemandOverCellularOnly
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
}
default:
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self.activateOnDemandOption = activateOnDemandOption
if activateOnDemandOption == .none {
isActivateOnDemandEnabled = false
} else {
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
}
}
}
extension ActivateOnDemandSetting {
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
}
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
@objc enum TunnelStatus: Int {
case inactive
case activating
case active
case deactivating
case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case waiting // Waiting for another tunnel to be brought down
init(from systemStatus: NEVPNStatus) {
switch systemStatus {
case .connected:
self = .active
case .connecting:
self = .activating
case .disconnected:
self = .inactive
case .disconnecting:
self = .deactivating
case .reasserting:
self = .reasserting
case .invalid:
self = .inactive
}
}
}
extension TunnelStatus: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .inactive: return "inactive"
case .activating: return "activating"
case .active: return "active"
case .deactivating: return "deactivating"
case .reasserting: return "reasserting"
case .restarting: return "restarting"
case .waiting: return "waiting"
}
}
}
@@ -0,0 +1,495 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os.log
protocol TunnelsManagerListDelegate: class {
func tunnelAdded(at index: Int)
func tunnelModified(at index: Int)
func tunnelMoved(from oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at index: Int)
}
protocol TunnelsManagerActivationDelegate: class {
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
}
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
case tunnelIsNotInactive
case anotherTunnelIsOperational(otherTunnelName: String)
case failedWhileStarting // startTunnel() throwed
case failedWhileSaving // save config after re-enabling throwed
case failedWhileLoading // reloading config throwed
case failedBecauseOfTooManyErrors // recursion limit reached
var alertText: AlertText {
switch self {
case .tunnelIsNotInactive:
return ("Activation failure", "The tunnel is already active or in the process of being activated")
case .anotherTunnelIsOperational(let otherTunnelName):
return ("Activation failure", "Please disconnect '\(otherTunnelName)' before enabling this tunnel.")
case .failedWhileStarting, .failedWhileSaving, .failedWhileLoading, .failedBecauseOfTooManyErrors:
return ("Activation failure", "The tunnel could not be activated.")
}
}
}
enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed
case activationFailedWithExtensionError(title: String, message: String)
var alertText: AlertText {
switch self {
case .activationFailed:
return ("Activation failure", "The tunnel could not be activated. Please ensure that you are connected to the Internet.")
case .activationFailedWithExtensionError(let title, let message):
return (title, message)
}
}
}
enum TunnelsManagerError: WireGuardAppError {
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case systemErrorOnListingTunnels
case systemErrorOnAddTunnel
case systemErrorOnModifyTunnel
case systemErrorOnRemoveTunnel
var alertText: AlertText {
switch self {
case .tunnelNameEmpty:
return ("No name provided", "Cannot create tunnel with an empty name")
case .tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists")
case .systemErrorOnListingTunnels:
return ("Unable to list tunnels", "")
case .systemErrorOnAddTunnel:
return ("Unable to create tunnel", "")
case .systemErrorOnModifyTunnel:
return ("Unable to modify tunnel", "")
case .systemErrorOnRemoveTunnel:
return ("Unable to remove tunnel", "")
}
}
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject?
init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
startObservingTunnelStatuses()
}
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) {
#if targetEnvironment(simulator)
completionHandler(.success(TunnelsManager(tunnelProviders: [])))
#else
NETunnelProviderManager.loadAllFromPreferences { managers, error in
if let error = error {
wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels))
return
}
completionHandler(.success(TunnelsManager(tunnelProviders: managers ?? [])))
}
#endif
}
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
return
}
if tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
return
}
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel))
return
}
guard let self = self else { return }
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel))
}
}
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
}
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful)
return
}
let tail = tunnelConfigurations.dropFirst()
add(tunnelConfiguration: head) { [weak self, tail] result in
DispatchQueue.main.async {
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler)
}
}
}
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty)
return
}
let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = (tunnelName != tunnelProviderManager.localizedDescription)
if isNameChanged {
if tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return
}
tunnel.name = tunnelName
}
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = (!tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled)
activateOnDemandSetting.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
return
}
guard let self = self else { return }
if isNameChanged {
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { $0.name < $1.name }
let newIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
}
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
// Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.status = .restarting
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
if isActivatingOnDemand {
// Reload tunnel after saving.
// Without this, the tunnel stopes getting updates on the tunnel status from iOS.
tunnelProviderManager.loadFromPreferences { error in
tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
guard error == nil else {
wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel)
return
}
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider
tunnelProviderManager.removeFromPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel)
return
}
if let self = self {
let index = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.remove(at: index)
self.tunnelsListDelegate?.tunnelRemoved(at: index)
}
completionHandler(nil)
}
}
func numberOfTunnels() -> Int {
return tunnels.count
}
func tunnel(at index: Int) -> TunnelContainer {
return tunnels[index]
}
func tunnel(named tunnelName: String) -> TunnelContainer? {
return tunnels.first { $0.name == tunnelName }
}
func startActivation(of tunnel: TunnelContainer) {
guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
guard tunnel.status == .inactive else {
activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
return
}
if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
alreadyWaitingTunnel.status = .inactive
}
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
tunnel.status = .waiting
if tunnelInOperation.status != .deactivating {
startDeactivation(of: tunnelInOperation)
}
return
}
tunnel.startActivation(activationDelegate: activationDelegate)
}
func startDeactivation(of tunnel: TunnelContainer) {
tunnel.isAttemptingActivation = false
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
tunnel.startDeactivation()
}
func refreshStatuses() {
tunnels.forEach { $0.refreshStatus() }
}
private func startObservingTunnelStatuses() {
guard statusObservationToken == nil else { return }
statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager,
let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
// Track what happened to our attempt to start the tunnel
if tunnel.isAttemptingActivation {
if session.status == .connected {
tunnel.isAttemptingActivation = false
self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
} else if session.status == .disconnected {
tunnel.isAttemptingActivation = false
if let (title, message) = self.lastErrorTextFromNetworkExtension(for: tunnel) {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message))
} else {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed)
}
}
}
// In case we're restarting the tunnel
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
// Don't change tunnel.status when disconnecting for a restart
if session.status == .disconnected {
tunnel.startActivation(activationDelegate: self.activationDelegate)
}
return
}
tunnel.refreshStatus()
// In case some other tunnel is waiting for this tunnel to get deactivated
if session.status == .disconnected || session.status == .invalid {
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
}
}
}
}
func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
guard let lastErrorText = String(data: lastErrorData, encoding: .utf8) else { return nil }
let lastErrorStrings = lastErrorText.split(separator: "\n").map { String($0) }
guard lastErrorStrings.count == 3 else { return nil }
let attemptIdInDisk = lastErrorStrings[0]
if let attemptIdForTunnel = tunnel.activationAttemptId, attemptIdInDisk == attemptIdForTunnel {
return (title: lastErrorStrings[1], message: lastErrorStrings[2])
}
return nil
}
deinit {
if let statusObservationToken = statusObservationToken {
NotificationCenter.default.removeObserver(statusObservationToken)
}
}
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool
var isAttemptingActivation = false {
didSet {
if isAttemptingActivation {
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
guard let self = self else { return }
self.refreshStatus()
if self.status == .inactive || self.status == .active {
self.isAttemptingActivation = false // This also invalidates the timer
}
}
self.activationTimer = activationTimer
RunLoop.main.add(activationTimer, forMode: .default)
} else {
activationTimer?.invalidate()
activationTimer = nil
}
}
}
var activationAttemptId: String?
var activationTimer: Timer?
fileprivate let tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus?
init(tunnel: NETunnelProviderManager) {
name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status)
self.status = status
isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
tunnelProvider = tunnel
super.init()
}
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.tunnelConfiguration()
}
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshStatus() {
let status = TunnelStatus(from: tunnelProvider.connection.status)
self.status = status
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
}
//swiftlint:disable:next function_body_length
fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
if recursionCount >= 8 {
wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors)
return
}
wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
guard tunnelProvider.isEnabled else {
// In case the tunnel had gotten disabled, re-enable and save it,
// then call this function again.
wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
tunnelProvider.isEnabled = true
tunnelProvider.saveToPreferences { [weak self] error in
guard let self = self else { return }
if error != nil {
wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving)
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown),
activationDelegate: activationDelegate)
}
return
}
// Start the tunnel
do {
wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
isAttemptingActivation = true
let activationAttemptId = UUID().uuidString
self.activationAttemptId = activationAttemptId
try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
wg_log(.debug, staticMessage: "startActivation: Success")
activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
} catch let error {
isAttemptingActivation = false
guard let systemError = error as? NEVPNError else {
wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
return
}
guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting)
return
}
wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
tunnelProvider.loadFromPreferences { [weak self] error in
guard let self = self else { return }
if error != nil {
wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
self.status = .inactive
activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading)
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded")
wg_log(.debug, staticMessage: "startActivation: Invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
}
}
}
fileprivate func startDeactivation() {
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
}
extension NEVPNStatus: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .connected: return "connected"
case .connecting: return "connecting"
case .disconnected: return "disconnected"
case .disconnecting: return "disconnecting"
case .reasserting: return "reasserting"
case .invalid: return "invalid"
}
}
}
+97 -66
View File
@@ -3,6 +3,7 @@
import UIKit
//swiftlint:disable:next type_body_length
class TunnelViewModel {
enum InterfaceField: String {
@@ -37,13 +38,13 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44
class InterfaceData {
var scratchpad: [InterfaceField: String] = [:]
var fieldsWithError: Set<InterfaceField> = []
var scratchpad = [InterfaceField: String]()
var fieldsWithError = Set<InterfaceField>()
var validatedConfiguration: InterfaceConfiguration?
subscript(field: InterfaceField) -> String {
get {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad()
@@ -51,21 +52,19 @@ class TunnelViewModel {
return scratchpad[field] ?? ""
}
set(stringValue) {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
if stringValue.isEmpty {
scratchpad.removeValue(forKey: field)
} else {
scratchpad[field] = stringValue
}
if (field == .privateKey) {
if (stringValue.count == TunnelViewModel.keyLengthInBase64),
let privateKey = Data(base64Encoded: stringValue),
privateKey.count == 32 {
if field == .privateKey {
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Encoded: stringValue), privateKey.count == TunnelConfiguration.keyLength {
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
scratchpad[.publicKey] = publicKey.base64EncodedString()
} else {
@@ -81,7 +80,7 @@ class TunnelViewModel {
scratchpad[.name] = config.name
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if (!config.addresses.isEmpty) {
if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
}
if let listenPort = config.listenPort {
@@ -90,11 +89,12 @@ class TunnelViewModel {
if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu)
}
if (!config.dns.isEmpty) {
if !config.dns.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
}
}
//swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
@@ -109,14 +109,14 @@ class TunnelViewModel {
fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required")
}
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == 32 else {
guard let privateKey = Data(base64Encoded: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.privateKey)
return .error("Interface's private key must be a 32-byte key in base64 encoding")
}
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
var errorMessages: [String] = []
var errorMessages = [String]()
if let addressesString = scratchpad[.addresses] {
var addresses: [IPAddressRange] = []
var addresses = [IPAddressRange]()
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let address = IPAddressRange(from: trimmedString) {
@@ -145,7 +145,7 @@ class TunnelViewModel {
}
}
if let dnsString = scratchpad[.dns] {
var dnsServers: [DNSServer] = []
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let dnsServer = DNSServer(from: trimmedString) {
@@ -158,16 +158,15 @@ class TunnelViewModel {
config.dns = dnsServers
}
guard (errorMessages.isEmpty) else {
return .error(errorMessages.first!)
}
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config
return .saved(config)
}
func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
return interfaceFields.filter { (field) -> Bool in
if (TunnelViewModel.interfaceFieldsWithControl.contains(field)) {
return interfaceFields.filter { field in
if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
return true
}
return (!self[field].isEmpty)
@@ -178,14 +177,14 @@ class TunnelViewModel {
class PeerData {
var index: Int
var scratchpad: [PeerField: String] = [:]
var fieldsWithError: Set<PeerField> = []
var scratchpad = [PeerField: String]()
var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration?
// For exclude private IPs
var shouldAllowExcludePrivateIPsControl: Bool = false /* Read-only from the VC's point of view */
var excludePrivateIPsValue: Bool = false /* Read-only from the VC's point of view */
fileprivate var numberOfPeers: Int = 0
private(set) var shouldAllowExcludePrivateIPsControl = false
private(set) var excludePrivateIPsValue = false
fileprivate var numberOfPeers = 0
init(index: Int) {
self.index = index
@@ -193,7 +192,7 @@ class TunnelViewModel {
subscript(field: PeerField) -> String {
get {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to read a config, setup the scratchpad.
// The scratchpad shall serve as a cache of what we want to show in the UI.
populateScratchpad()
@@ -201,18 +200,18 @@ class TunnelViewModel {
return scratchpad[field] ?? ""
}
set(stringValue) {
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
// When starting to edit a config, setup the scratchpad and remove the configuration.
// The scratchpad shall be the sole source of the being-edited configuration.
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
if stringValue.isEmpty {
scratchpad.removeValue(forKey: field)
} else {
scratchpad[field] = stringValue
}
if (field == .allowedIPs) {
if field == .allowedIPs {
updateExcludePrivateIPsFieldState()
}
}
@@ -225,7 +224,7 @@ class TunnelViewModel {
if let preSharedKey = config.preSharedKey {
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
}
if (!config.allowedIPs.isEmpty) {
if !config.allowedIPs.isEmpty {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
}
if let endpoint = config.endpoint {
@@ -237,6 +236,7 @@ class TunnelViewModel {
updateExcludePrivateIPsFieldState()
}
//swiftlint:disable:next cyclomatic_complexity
func save() -> SaveResult<PeerConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
@@ -247,14 +247,14 @@ class TunnelViewModel {
fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required")
}
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == 32 else {
guard let publicKey = Data(base64Encoded: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
fieldsWithError.insert(.publicKey)
return .error("Peer's public key must be a 32-byte key in base64 encoding")
}
var config = PeerConfiguration(publicKey: publicKey)
var errorMessages: [String] = []
var errorMessages = [String]()
if let preSharedKeyString = scratchpad[.preSharedKey] {
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == 32 {
if let preSharedKey = Data(base64Encoded: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength {
config.preSharedKey = preSharedKey
} else {
fieldsWithError.insert(.preSharedKey)
@@ -262,7 +262,7 @@ class TunnelViewModel {
}
}
if let allowedIPsString = scratchpad[.allowedIPs] {
var allowedIPs: [IPAddressRange] = []
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let allowedIP = IPAddressRange(from: trimmedString) {
@@ -291,16 +291,15 @@ class TunnelViewModel {
}
}
guard (errorMessages.isEmpty) else {
return .error(errorMessages.first!)
}
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config
return .saved(config)
}
func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
return peerFields.filter { (field) -> Bool in
if (TunnelViewModel.peerFieldsWithControl.contains(field)) {
return peerFields.filter { field in
if TunnelViewModel.peerFieldsWithControl.contains(field) {
return true
}
return (!self[field].isEmpty)
@@ -319,12 +318,12 @@ class TunnelViewModel {
]
func updateExcludePrivateIPsFieldState() {
guard (numberOfPeers == 1) else {
guard numberOfPeers == 1 else {
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false
return
}
if (scratchpad.isEmpty) {
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(
@@ -332,10 +331,10 @@ class TunnelViewModel {
.split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
)
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) {
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = false
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) {
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = true
} else {
@@ -353,14 +352,13 @@ class TunnelViewModel {
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
let modifiedAllowedIPStrings: [String]
if (isOn) {
modifiedAllowedIPStrings = ipv6Addresses +
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
if isOn {
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
} else {
modifiedAllowedIPStrings = ipv6Addresses +
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
}
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
excludePrivateIPsValue = isOn
}
}
@@ -375,11 +373,11 @@ class TunnelViewModel {
init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData: InterfaceData = InterfaceData()
var peersData: [PeerData] = []
var peersData = [PeerData]()
if let tunnelConfiguration = tunnelConfiguration {
interfaceData.validatedConfiguration = tunnelConfiguration.interface
for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: i)
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: index)
peerData.validatedConfiguration = peerConfiguration
peersData.append(peerData)
}
@@ -396,22 +394,22 @@ class TunnelViewModel {
func appendEmptyPeer() {
let peer = PeerData(index: peersData.count)
peersData.append(peer)
for p in peersData {
p.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState()
for peer in peersData {
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
}
func deletePeer(peer: PeerData) {
let removedPeer = peersData.remove(at: peer.index)
assert(removedPeer.index == peer.index)
for p in peersData[peer.index ..< peersData.count] {
assert(p.index > 0)
p.index = p.index - 1
for peer in peersData[peer.index ..< peersData.count] {
assert(peer.index > 0)
peer.index -= 1
}
for p in peersData {
p.numberOfPeers = peersData.count
p.updateExcludePrivateIPsFieldState()
for peer in peersData {
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
}
@@ -420,14 +418,14 @@ class TunnelViewModel {
let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() }
// Collate the results
switch (interfaceSaveResult) {
switch interfaceSaveResult {
case .error(let errorMessage):
return .error(errorMessage)
case .saved(let interfaceConfiguration):
var peerConfigurations: [PeerConfiguration] = []
var peerConfigurations = [PeerConfiguration]()
peerConfigurations.reserveCapacity(peerSaveResults.count)
for peerSaveResult in peerSaveResults {
switch (peerSaveResult) {
switch peerSaveResult {
case .error(let errorMessage):
return .error(errorMessage)
case .saved(let peerConfiguration):
@@ -437,13 +435,46 @@ class TunnelViewModel {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
if peerPublicKeysArray.count != peerPublicKeysSet.count {
return .error("Two or more peers cannot have the same public key")
}
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration)
tunnelConfiguration.peers = peerConfigurations
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
return .saved(tunnelConfiguration)
}
}
}
// MARK: Activate on demand
extension TunnelViewModel {
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch activateOnDemandOption {
case .none:
return "Off"
case .useOnDemandOverWiFiOrCellular:
return "Wi-Fi or cellular"
case .useOnDemandOverWiFiOnly:
return "Wi-Fi only"
case .useOnDemandOverCellularOnly:
return "Cellular only"
}
}
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting {
if activateOnDemandSetting.isActivateOnDemandEnabled {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
}
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
}
}
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
return .useOnDemandOverWiFiOrCellular
}
}
+39 -9
View File
@@ -11,10 +11,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var mainVC: MainViewController?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path)
let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white
window.backgroundColor = .white
self.window = window
let mainVC = MainViewController()
@@ -27,14 +28,43 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
defer {
do {
try FileManager.default.removeItem(at: url)
} catch {
os_log("Failed to remove item from Inbox: %{public}@", log: OSLog.default, type: .debug, url.absoluteString)
}
mainVC?.tunnelsListVC?.importFromFile(url: url) {
_ = FileManager.deleteFile(at: url)
}
mainVC?.tunnelsListVC?.importFromFile(url: url)
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
mainVC?.refreshTunnelConnectionStatuses()
}
}
// MARK: State restoration
extension AppDelegate {
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication,
viewControllerWithRestorationIdentifierPath identifierComponents: [String],
coder: NSCoder) -> UIViewController? {
guard let vcIdentifier = identifierComponents.last else { return nil }
if vcIdentifier.hasPrefix("TunnelDetailVC:") {
let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
if let tunnelsManager = mainVC?.tunnelsManager {
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
return TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
}
} else {
// Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
}
}
return nil
}
}
@@ -1,55 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class CopyableLabelTableViewCell: UITableViewCell {
var copyableGesture = true
var textToCopy: String? {
fatalError("textToCopy must be implemented by subclass")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
self.addGestureRecognizer(gestureRecognizer)
self.isUserInteractionEnabled = true
}
// MARK: - UIGestureRecognizer
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !self.copyableGesture {
return
}
guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared
menuController.setTargetRect(self.detailTextLabel?.frame ?? recognizerView.frame, in: self.detailTextLabel?.superview ?? recognizerSuperView)
menuController.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = textToCopy
}
override func prepareForReuse() {
super.prepareForReuse()
self.copyableGesture = true
}
}
@@ -5,55 +5,15 @@ import UIKit
import os.log
class ErrorPresenter {
static func errorMessage(for error: Error) -> (String, String)? {
switch (error) {
// TunnelManagementError
case TunnelManagementError.tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists")
case TunnelManagementError.tunnelInvalidName:
return ("Name already exists", "The tunnel name is invalid")
case TunnelManagementError.vpnSystemErrorOnAddTunnel:
return ("Unable to create tunnel", "Internal error")
case TunnelManagementError.vpnSystemErrorOnModifyTunnel:
return ("Unable to modify tunnel", "Internal error")
case TunnelManagementError.vpnSystemErrorOnRemoveTunnel:
return ("Unable to remove tunnel", "Internal error")
// TunnelActivationError
case TunnelActivationError.dnsResolutionFailed:
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
case TunnelActivationError.tunnelActivationFailed:
return ("Activation failure", "The tunnel could not be activated due to an internal error")
case TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(let otherTunnelStatus):
let statusString: String = {
switch (otherTunnelStatus) {
case .active: fallthrough
case .reasserting: fallthrough
case .restarting:
return "active"
case .activating: fallthrough
case .resolvingEndpointDomains:
return "being activated"
case .deactivating:
return "being deactivated"
case .inactive:
fatalError()
}
}()
return ("Activation failure", "Another tunnel is currently \(statusString)")
default:
os_log("ErrorPresenter: Error not presented: %{public}@", log: OSLog.default, type: .error, "\(error)")
return nil
}
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
let (title, message) = error.alertText
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
}
static func showErrorAlert(error: Error, from sourceVC: UIViewController?,
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
guard let (title, message) = ErrorPresenter.errorMessage(for: error) else { return }
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
onDismissal?()
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
@@ -61,15 +21,4 @@ class ErrorPresenter {
sourceVC.present(alert, animated: true, completion: onPresented)
}
static func showErrorAlert(title: String, message: String, from sourceVC: UIViewController?, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
onDismissal?()
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
sourceVC.present(alert, animated: true)
}
}
@@ -1,55 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class MainViewController: UISplitViewController {
var tunnelsListVC: TunnelsListTableViewController?
init() {
let detailVC = UIViewController()
detailVC.view.backgroundColor = UIColor.white
let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController()
let masterNC = UINavigationController(rootViewController: masterVC)
self.tunnelsListVC = masterVC
super.init(nibName: nil, bundle: nil)
self.viewControllers = [ masterNC, detailNC ]
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
self.delegate = self
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
self.preferredDisplayMode = .allVisible
}
func openForEditing(configFileURL: URL) {
tunnelsListVC?.openForEditing(configFileURL: configFileURL)
}
}
extension MainViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController) -> Bool {
// On iPhone, if the secondaryVC (detailVC) is just a UIViewController, it indicates that it's empty,
// so just show the primaryVC (masterVC).
let detailVC = (secondaryViewController as? UINavigationController)?.viewControllers.first
let isDetailVCEmpty: Bool
if let detailVC = detailVC {
isDetailVCEmpty = (type(of: detailVC) == UIViewController.self)
} else {
isDetailVCEmpty = true
}
return isDetailVCEmpty
}
}
@@ -1,50 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class ScrollableLabel: UIScrollView {
var text: String {
get { return label.text ?? "" }
set(value) { label.text = value }
}
var textColor: UIColor {
get { return label.textColor }
set(value) { label.textColor = value }
}
private let label: UILabel
init() {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .right
self.label = label
super.init(frame: CGRect.zero)
self.isDirectionalLockEnabled = true
self.showsHorizontalScrollIndicator = false
self.showsVerticalScrollIndicator = false
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.leftAnchor.constraint(equalTo: self.contentLayoutGuide.leftAnchor),
label.topAnchor.constraint(equalTo: self.contentLayoutGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor),
label.rightAnchor.constraint(equalTo: self.contentLayoutGuide.rightAnchor),
label.heightAnchor.constraint(equalTo: self.heightAnchor),
])
// If label has less content, it should expand to fit the scrollView,
// so that right-alignment works in the label.
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: label, attribute: .width, relatedBy: .equal,
toItem: self, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -1,228 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
enum SettingsFields: String {
case iosAppVersion = "WireGuard for iOS"
case goBackendVersion = "WireGuard Go Backend"
case exportZipArchive = "Export zip archive"
}
let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion],
[.exportZipArchive]
]
let tunnelsManager: TunnelsManager?
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
init(tunnelsManager: TunnelsManager?) {
self.tunnelsManager = tunnelsManager
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Settings"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
self.tableView.rowHeight = 44
self.tableView.allowsSelection = false
self.tableView.register(TunnelSettingsTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
self.tableView.register(TunnelSettingsTableViewButtonCell.self, forCellReuseIdentifier: TunnelSettingsTableViewButtonCell.id)
let logo = UIImageView(image: UIImage(named: "wireguard.pdf", in: Bundle.main, compatibleWith: nil)!)
logo.contentMode = .scaleAspectFit
let height = self.tableView.rowHeight * 1.5
let width = height * logo.image!.size.width / logo.image!.size.height
logo.frame = CGRect(x: 0, y: 0, width: width, height: height)
logo.bounds = logo.frame.insetBy(dx: 2, dy: 2)
self.tableView.tableFooterView = logo
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let logo = self.tableView.tableFooterView else { return }
let bottomPadding = max(self.tableView.layoutMargins.bottom, CGFloat(10))
let fullHeight = max(self.tableView.contentSize.height, self.tableView.bounds.size.height - self.tableView.layoutMargins.top - bottomPadding)
let e = logo.frame
logo.frame = CGRect(x: e.minX, y: fullHeight - e.height, width: e.width, height: e.height)
}
@objc func doneTapped() {
dismiss(animated: true, completion: nil)
}
func exportConfigurationsAsZipFile(sourceView: UIView) {
guard let tunnelsManager = tunnelsManager, tunnelsManager.numberOfTunnels() > 0 else {
showErrorAlert(title: "Nothing to export", message: "There are no tunnels to export")
return
}
var inputsToArchiver: [(fileName: String, contents: Data)] = []
var usedNames: Set<String> = []
for i in 0 ..< tunnelsManager.numberOfTunnels() {
guard let tunnelConfiguration = tunnelsManager.tunnel(at: i).tunnelConfiguration() else { continue }
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
let name = tunnelConfiguration.interface.name
var nameToCheck = name
var i = 0
while (usedNames.contains(nameToCheck)) {
i = i + 1
nameToCheck = "\(name)\(i)"
}
usedNames.insert(nameToCheck)
inputsToArchiver.append((fileName: "\(nameToCheck).conf", contents: contents))
}
}
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return
}
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
do {
try FileManager.default.removeItem(at: destinationURL)
} catch {
os_log("Failed to delete file: %{public}@ : %{public}@", log: OSLog.default, type: .error, destinationURL.absoluteString, error.localizedDescription)
}
do {
try ZipArchive.archive(inputs: inputsToArchiver, to: destinationURL)
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
// popoverPresentationController shall be non-nil on the iPad
activityVC.popoverPresentationController?.sourceView = sourceView
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
present(activityVC, animated: true)
} catch (let error) {
showErrorAlert(title: "Unable to export", message: "There was an error exporting the tunnel configuration archive: \(String(describing: error))")
}
}
func showErrorAlert(title: String, message: String) {
let okAction = UIAlertAction(title: "OK", style: .default)
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
}
// MARK: UITableViewDataSource
extension SettingsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return settingsFieldsBySection.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settingsFieldsBySection[section].count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch (section) {
case 0:
return "About"
case 1:
return "Export configurations"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
if (field == .iosAppVersion || field == .goBackendVersion) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewKeyValueCell.id, for: indexPath) as! TunnelSettingsTableViewKeyValueCell
cell.key = field.rawValue
if (field == .iosAppVersion) {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
cell.value = appVersion
} else if (field == .goBackendVersion) {
cell.value = WIREGUARD_GO_VERSION
}
return cell
} else {
assert(field == .exportZipArchive)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
}
return cell
}
}
}
class TunnelSettingsTableViewKeyValueCell: UITableViewCell {
static let id: String = "TunnelSettingsTableViewKeyValueCell"
var key: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var value: String {
get { return detailTextLabel?.text ?? "" }
set(value) { detailTextLabel?.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: TunnelSettingsTableViewKeyValueCell.id)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelSettingsTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelSettingsTableViewButtonCell"
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
let button: UIButton
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
}
}
@@ -7,6 +7,14 @@ import UIKit
class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
@@ -20,12 +28,14 @@ class TunnelDetailTableViewController: UITableViewController {
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
@@ -37,11 +47,25 @@ class TunnelDetailTableViewController: UITableViewController {
self.title = tunnelViewModel.interfaceData[.name]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
self.tableView.rowHeight = 44
self.tableView.estimatedRowHeight = 44
self.tableView.rowHeight = UITableView.automaticDimension
self.tableView.allowsSelection = false
self.tableView.register(TunnelDetailTableViewStatusCell.self, forCellReuseIdentifier: TunnelDetailTableViewStatusCell.id)
self.tableView.register(TunnelDetailTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
self.tableView.register(TunnelDetailTableViewButtonCell.self, forCellReuseIdentifier: TunnelDetailTableViewButtonCell.id)
self.tableView.register(StatusCell.self)
self.tableView.register(KeyValueCell.self)
self.tableView.register(ButtonCell.self)
self.tableView.register(ActivateOnDemandCell.self)
// State restoration
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
private func loadSections() {
sections.removeAll()
sections.append(.status)
sections.append(.interface)
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.onDemand)
sections.append(.delete)
}
@objc func editTapped() {
@@ -52,17 +76,9 @@ class TunnelDetailTableViewController: UITableViewController {
present(editNC, animated: true)
}
func showErrorAlert(title: String, message: String) {
let okAction = UIAlertAction(title: "OK", style: .default)
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
@@ -83,6 +99,7 @@ class TunnelDetailTableViewController: UITableViewController {
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
loadSections()
self.title = tunnel.name
self.tableView.reloadData()
}
@@ -95,125 +112,117 @@ extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 3 + tunnelViewModel.peersData.count
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
switch sections[section] {
case .status:
return 1
} else if (section == 1) {
// Interface
return interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerData = tunnelViewModel.peersData[section - 2]
case .interface:
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
case .peer(let peerData):
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else {
// Delete tunnel
case .onDemand:
return 1
case .delete:
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
switch sections[section] {
case .status:
return "Status"
} else if (section == 1) {
// Interface
case .interface:
return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
case .peer:
return "Peer"
} else {
// Delete tunnel
case .onDemand:
return "On-Demand Activation"
case .delete:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let interfaceData = tunnelViewModel.interfaceData
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section == 0) {
// Status
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewStatusCell.id, for: indexPath) as! TunnelDetailTableViewStatusCell
cell.tunnel = self.tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let s = self else { return }
if (isOn) {
s.tunnelsManager.startActivation(of: s.tunnel) { [weak s] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: s)
DispatchQueue.main.async {
cell.statusSwitch.isOn = false
}
}
}
} else {
s.tunnelsManager.startDeactivation(of: s.tunnel) { error in
print("Error while deactivating: \(String(describing: error))")
}
}
}
return cell
} else if (section == 1) {
// Interface
let field = interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
// Set key and value
cell.key = field.rawValue
cell.value = interfaceData[field]
return cell
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
let peerData = tunnelViewModel.peersData[section - 2]
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[row]
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewKeyValueCell.id, for: indexPath) as! TunnelDetailTableViewKeyValueCell
// Set key and value
cell.key = field.rawValue
cell.value = peerData[field]
return cell
} else {
assert(section == (2 + numberOfPeerSections))
// Delete configuration
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewButtonCell.id, for: indexPath) as! TunnelDetailTableViewButtonCell
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let s = self else { return }
s.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak s] in
guard let tunnelsManager = s?.tunnelsManager, let tunnel = s?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { (error) in
if (error != nil) {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
s?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
switch sections[indexPath.section] {
case .status:
return statusCell(for: tableView, at: indexPath)
case .interface:
return interfaceCell(for: tableView, at: indexPath)
case .peer(let peer):
return peerCell(for: tableView, at: indexPath, with: peer)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
case .delete:
return deleteConfigurationCell(for: tableView, at: indexPath)
}
}
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: StatusCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
if isOn {
self.tunnelsManager.startActivation(of: self.tunnel)
} else {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
return cell
}
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = peerData[field]
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ActivateOnDemandCell = tableView.dequeueReusableCell(for: indexPath)
cell.tunnel = self.tunnel
return cell
}
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
self?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
}
}
class TunnelDetailTableViewStatusCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewStatusCell"
private class StatusCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
@@ -230,14 +239,14 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
statusSwitch = UISwitch()
super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
super.init(style: .default, reuseIdentifier: KeyValueCell.reuseIdentifier)
accessoryView = statusSwitch
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
if (isOnSwitchToggledHandlerEnabled) {
if isOnSwitchToggledHandlerEnabled {
onSwitchToggled?(statusSwitch.isOn)
}
}
@@ -248,7 +257,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
return
}
let text: String
switch (status) {
switch status {
case .inactive:
text = "Inactive"
case .activating:
@@ -259,10 +268,10 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
text = "Deactivating"
case .reasserting:
text = "Reactivating"
case .resolvingEndpointDomains:
text = "Resolving domains"
case .restarting:
text = "Restarting"
case .waiting:
text = "Waiting"
}
textLabel?.text = text
DispatchQueue.main.async { [weak statusSwitch] in
@@ -290,8 +299,7 @@ class TunnelDetailTableViewStatusCell: UITableViewCell {
}
}
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell"
private class KeyValueCell: CopyableLabelTableViewCell {
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
@@ -308,9 +316,17 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
let keyLabel: UILabel
let valueLabel: ScrollableLabel
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
valueLabel = ScrollableLabel()
valueLabel.label.font = UIFont.preferredFont(forTextStyle: .body)
valueLabel.label.adjustsFontForContentSizeCategory = true
keyLabel.textColor = UIColor.black
valueLabel.textColor = UIColor.gray
@@ -322,22 +338,55 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
keyLabel.textAlignment = .left
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 8)
])
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabel.bottomAnchor, multiplier: 0.5)
])
// Key label should never appear truncated
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
// Key label should hug it's content; value label should not.
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
configureForContentSize()
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if self.traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabel.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(self.contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
self.contentSizeBasedConstraints = constraints
}
}
required init?(coder aDecoder: NSCoder) {
@@ -348,11 +397,11 @@ class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
super.prepareForReuse()
key = ""
value = ""
configureForContentSize()
}
}
class TunnelDetailTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewButtonCell"
private class ButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
@@ -368,14 +417,17 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@@ -394,3 +446,39 @@ class TunnelDetailTableViewButtonCell: UITableViewCell {
hasDestructiveAction = false
}
}
private class ActivateOnDemandCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.activateOnDemandSetting())
onDemandStatusObservervationToken = tunnel?.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
self?.update(from: tunnel.activateOnDemandSetting())
}
}
}
var onDemandStatusObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
textLabel?.text = "Activate on demand"
textLabel?.font = UIFont.preferredFont(forTextStyle: .body)
textLabel?.adjustsFontForContentSizeCategory = true
detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .body)
detailTextLabel?.adjustsFontForContentSizeCategory = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(from activateOnDemandSetting: ActivateOnDemandSetting?) {
detailTextLabel?.text = TunnelViewModel.activateOnDemandDetailText(for: activateOnDemandSetting)
}
override func prepareForReuse() {
super.prepareForReuse()
textLabel?.text = "Activate on demand"
detailTextLabel?.text = ""
}
}
@@ -1,666 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
protocol TunnelEditTableViewControllerDelegate: class {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}
// MARK: TunnelEditTableViewController
class TunnelEditTableViewController: UITableViewController {
weak var delegate: TunnelEditTableViewControllerDelegate?
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
[.name],
[.privateKey, .publicKey, .generateKeyPair],
[.addresses, .listenPort, .mtu, .dns]
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
.deletePeer
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
super.init(style: .grouped)
}
init(tunnelsManager tm: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
// Use this initializer to create a new tunnel.
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
tunnelsManager = tm
tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = (tunnel == nil) ? "New configuration" : "Edit configuration"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
self.tableView.rowHeight = 44
self.tableView.allowsSelection = false
self.tableView.register(TunnelEditTableViewKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewKeyValueCell.id)
self.tableView.register(TunnelEditTableViewReadOnlyKeyValueCell.self, forCellReuseIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id)
self.tableView.register(TunnelEditTableViewButtonCell.self, forCellReuseIdentifier: TunnelEditTableViewButtonCell.id)
self.tableView.register(TunnelEditTableViewSwitchCell.self, forCellReuseIdentifier: TunnelEditTableViewSwitchCell.id)
}
@objc func saveTapped() {
self.tableView.endEditing(false)
let tunnelSaveResult = tunnelViewModel.save()
switch (tunnelSaveResult) {
case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer"
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self)
self.tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration):
if let tunnel = tunnel {
// We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel, with: tunnelConfiguration) { [weak self] (error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
} else {
// We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] (tunnel, error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
self?.dismiss(animated: true, completion: nil)
if let tunnel = tunnel {
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
}
}
}
}
@objc func cancelTapped() {
dismiss(animated: true, completion: nil)
self.delegate?.tunnelEditingCancelled()
}
func showErrorAlert(title: String, message: String) {
let okAction = UIAlertAction(title: "OK", style: .default)
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
}
// MARK: UITableViewDataSource
extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
return numberOfInterfaceSections + numberOfPeerSections + 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) {
// Interface
return interfaceFieldsBySection[section].count
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
return peerFieldsToShow.count
} else {
// Add peer
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section < numberOfInterfaceSections) {
// Interface
return (section == 0) ? "Interface" : nil
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
return "Peer"
} else {
// Add peer
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section < numberOfInterfaceSections) {
// Interface
let interfaceData = tunnelViewModel.interfaceData
let field = interfaceFieldsBySection[section][row]
if (field == .generateKeyPair) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self, weak interfaceData] in
if let interfaceData = interfaceData, let s = self {
interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
if let privateKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .privateKey),
let publicKeyRow = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: section)
let publicKeyIndex = IndexPath(row: publicKeyRow, section: section)
s.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .automatic)
}
}
}
return cell
} else if (field == .publicKey) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewReadOnlyKeyValueCell.id, for: indexPath) as! TunnelEditTableViewReadOnlyKeyValueCell
cell.key = field.rawValue
cell.value = interfaceData[field]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue
// Set placeholder text
switch (field) {
case .name:
cell.placeholderText = "Required"
case .privateKey:
cell.placeholderText = "Required"
case .addresses:
cell.placeholderText = "Optional"
case .listenPort:
cell.placeholderText = "Automatic"
case .mtu:
cell.placeholderText = "Automatic"
case .dns:
cell.placeholderText = "Optional"
case .publicKey: break
case .generateKeyPair: break
}
// Set keyboardType
if (field == .mtu || field == .listenPort) {
cell.keyboardType = .numberPad
} else if (field == .addresses || field == .dns) {
cell.keyboardType = .numbersAndPunctuation
}
// Show erroring fields
cell.isValueValid = (!interfaceData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = interfaceData[field]
if (field == .dns) { // While editing DNS, you might directly set exclude private IPs
cell.onValueBeingEdited = { [weak interfaceData] value in
interfaceData?[field] = value
}
} else {
cell.onValueChanged = { [weak interfaceData] value in
interfaceData?[field] = value
}
}
// Compute public key live
if (field == .privateKey) {
cell.onValueBeingEdited = { [weak self, weak interfaceData] value in
if let interfaceData = interfaceData, let s = self {
interfaceData[.privateKey] = value
if let row = s.interfaceFieldsBySection[section].firstIndex(of: .publicKey) {
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
}
}
}
}
return cell
}
} else if ((numberOfPeerSections > 0) && (section < (numberOfInterfaceSections + numberOfPeerSections))) {
// Peer
let peerIndex = (section - numberOfInterfaceSections)
let peerData = tunnelViewModel.peersData[peerIndex]
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
let field = peerFieldsToShow[row]
if (field == .deletePeer) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = field.rawValue
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return }
guard let s = self else { return }
s.showConfirmationAlert(message: "Delete this peer?",
buttonTitle: "Delete", from: cell,
onConfirmed: { [weak s] in
guard let s = s else { return }
let removedSectionIndices = s.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
tableView.performBatchUpdates({
s.tableView.deleteSections(removedSectionIndices, with: .automatic)
if (shouldShowExcludePrivateIPs) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
s.tableView.insertRows(at: [rowIndexPath], with: .automatic)
}
}
})
})
}
return cell
} else if (field == .excludePrivateIPs) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] (isOn) in
guard let s = self else { return }
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: s.tunnelViewModel.interfaceData[.dns])
if let row = s.peerFields.firstIndex(of: .allowedIPs) {
s.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
}
}
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewKeyValueCell.id, for: indexPath) as! TunnelEditTableViewKeyValueCell
// Set key
cell.key = field.rawValue
// Set placeholder text
switch (field) {
case .publicKey:
cell.placeholderText = "Required"
case .preSharedKey:
cell.placeholderText = "Optional"
case .endpoint:
cell.placeholderText = "Optional"
case .allowedIPs:
cell.placeholderText = "Optional"
case .persistentKeepAlive:
cell.placeholderText = "Off"
case .excludePrivateIPs: break
case .deletePeer: break
}
// Set keyboardType
if (field == .persistentKeepAlive) {
cell.keyboardType = .numberPad
} else if (field == .allowedIPs) {
cell.keyboardType = .numbersAndPunctuation
}
// Show erroring fields
cell.isValueValid = (!peerData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = peerData[field]
if (field != .allowedIPs) {
cell.onValueChanged = { [weak peerData] value in
peerData?[field] = value
}
}
// Compute state of exclude private IPs live
if (field == .allowedIPs) {
cell.onValueBeingEdited = { [weak self, weak peerData] value in
if let peerData = peerData, let s = self {
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
peerData[.allowedIPs] = value
if (oldValue != peerData.shouldAllowExcludePrivateIPsControl) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
if (peerData.shouldAllowExcludePrivateIPsControl) {
s.tableView.insertRows(at: [IndexPath(row: row, section: section)], with: .automatic)
} else {
s.tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: .automatic)
}
}
}
}
}
}
return cell
}
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections))
// Add peer
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewButtonCell.id, for: indexPath) as! TunnelEditTableViewButtonCell
cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in
guard let s = self else { return }
let shouldHideExcludePrivateIPs = (s.tunnelViewModel.peersData.count == 1 &&
s.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
let addedSectionIndices = s.appendEmptyPeer()
tableView.performBatchUpdates({
tableView.insertSections(addedSectionIndices, with: .automatic)
if (shouldHideExcludePrivateIPs) {
if let row = s.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: numberOfInterfaceSections /* First peer section */)
s.tableView.deleteRows(at: [rowIndexPath], with: .automatic)
}
}
}, completion: nil)
}
return cell
}
}
func appendEmptyPeer() -> IndexSet {
let numberOfInterfaceSections = interfaceFieldsBySection.count
tunnelViewModel.appendEmptyPeer()
let addedPeerIndex = tunnelViewModel.peersData.count - 1
let addedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + addedPeerIndex))
return addedSectionIndices
}
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
let numberOfInterfaceSections = interfaceFieldsBySection.count
assert(peer.index < tunnelViewModel.peersData.count)
tunnelViewModel.deletePeer(peer: peer)
let removedSectionIndices = IndexSet(integer: (numberOfInterfaceSections + peer.index))
return removedSectionIndices
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView,
onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { (_) in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
self.present(alert, animated: true, completion: nil)
}
}
class TunnelEditTableViewKeyValueCell: UITableViewCell {
static let id: String = "TunnelEditTableViewKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var isValueValid: Bool = true {
didSet {
if (isValueValid) {
keyLabel.textColor = UIColor.black
} else {
keyLabel.textColor = UIColor.red
}
}
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
let keyLabel: UILabel
let valueTextField: UITextField
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueTextField = UITextField()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// The "Persistent Keepalive" key doesn't fit into 0.4 * width on the iPhone SE,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueTextField.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
valueTextField.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
])
valueTextField.delegate = self
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
}
}
extension TunnelEditTableViewKeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = (textField.text ?? "" != textFieldValueOnBeginEditing)
guard (isModified) else { return }
if let onValueChanged = onValueChanged {
onValueChanged(textField.text ?? "")
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}
class TunnelEditTableViewReadOnlyKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelEditTableViewReadOnlyKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) {keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueLabel = ScrollableLabel()
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textColor = UIColor.gray
valueLabel.textColor = UIColor.gray
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .right
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width,
relatedBy: .equal,
toItem: self, attribute: .width,
multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
widthRatioConstraint
])
contentView.addSubview(valueLabel)
valueLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
valueLabel.leftAnchor.constraint(equalTo: keyLabel.rightAnchor, constant: 16),
valueLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
])
}
override var textToCopy: String? {
return self.valueLabel.text
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelEditTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelEditTableViewButtonCell"
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == UIColor.red }
set(value) { button.tintColor = value ? UIColor.red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
button = UIButton(type: .system)
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}
class TunnelEditTableViewSwitchCell: UITableViewCell {
static let id: String = "TunnelEditTableViewSwitchCell"
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? UIColor.black : UIColor.gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView: UISwitch
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
switchView = UISwitch()
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isOn = false
}
}
@@ -1,491 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var busyIndicator: UIActivityIndicatorView?
var centeredAddButton: BorderedTextButton?
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
// Set up the navigation bar
self.title = "WireGuard"
let addButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
self.navigationItem.rightBarButtonItem = addButtonItem
let settingsButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
self.navigationItem.leftBarButtonItem = settingsButtonItem
// Set up the busy indicator
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
// Add the busyIndicator, centered
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
busyIndicator.startAnimating()
self.busyIndicator = busyIndicator
// Create the tunnels manager, and when it's ready, create the tableView
TunnelsManager.create { [weak self] tunnelsManager in
guard let tunnelsManager = tunnelsManager else { return }
guard let s = self else { return }
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.rowHeight = 60
tableView.separatorStyle = .none
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
s.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: s.view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: s.view.rightAnchor),
tableView.topAnchor.constraint(equalTo: s.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: s.view.bottomAnchor)
])
tableView.dataSource = s
tableView.delegate = s
s.tableView = tableView
// Add an add button, centered
let centeredAddButton = BorderedTextButton()
centeredAddButton.title = "Add a tunnel"
centeredAddButton.isHidden = true
s.view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: s.view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: s.view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
self?.addButtonTapped(sender: centeredAddButton)
}
s.centeredAddButton = centeredAddButton
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
busyIndicator.stopAnimating()
tunnelsManager.delegate = s
s.tunnelsManager = tunnelsManager
s.onTunnelsManagerReady?(tunnelsManager)
s.onTunnelsManagerReady = nil
}
}
@objc func addButtonTapped(sender: AnyObject) {
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] (_) in
self?.presentViewControllerForFileImport()
}
alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] (_) in
self?.presentViewControllerForScanningQRCode()
}
alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] (_) in
if let s = self, let tunnelsManager = s.tunnelsManager {
s.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
}
}
alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
if let sender = sender as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sender
} else if let sender = sender as? UIView {
alert.popoverPresentationController?.sourceView = sender
alert.popoverPresentationController?.sourceRect = sender.bounds
}
self.present(alert, animated: true, completion: nil)
}
@objc func settingsButtonTapped(sender: UIBarButtonItem!) {
if (self.tunnelsManager == nil) { return } // Do nothing until we've loaded the tunnels
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
let settingsNC = UINavigationController(rootViewController: settingsVC)
settingsNC.modalPresentationStyle = .formSheet
self.present(settingsNC, animated: true)
}
func openForEditing(configFileURL: URL) {
let tunnelConfiguration: TunnelConfiguration?
let name = configFileURL.deletingPathExtension().lastPathComponent
do {
let fileContents = try String(contentsOf: configFileURL)
try tunnelConfiguration = WgQuickConfigFileParser.parse(fileContents, name: name)
} catch (let error) {
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration: \(String(describing: error))")
return
}
tunnelConfiguration?.interface.name = name
if let tunnelsManager = tunnelsManager {
presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
} else {
onTunnelsManagerReady = { [weak self] tunnelsManager in
self?.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
}
}
}
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
self.present(editNC, animated: true)
}
func presentViewControllerForFileImport() {
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
filePicker.delegate = self
self.present(filePicker, animated: true)
}
func presentViewControllerForScanningQRCode() {
let scanQRCodeVC = QRScanViewController()
scanQRCodeVC.delegate = self
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
scanQRCodeNC.modalPresentationStyle = .fullScreen
self.present(scanQRCodeNC, animated: true)
}
func showErrorAlert(title: String, message: String) {
let okAction = UIAlertAction(title: "OK", style: .default)
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(okAction)
self.present(alert, animated: true, completion: nil)
}
func importFromFile(url: URL) {
if (url.pathExtension == "zip") {
var unarchivedFiles: [(fileName: String, contents: Data)] = []
do {
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
} catch ZipArchiveError.cantOpenInputZipFile {
showErrorAlert(title: "Unable to read zip archive", message: "The zip archive could not be read.")
return
} catch ZipArchiveError.badArchive {
showErrorAlert(title: "Unable to read zip archive", message: "Bad or corrupt zip archive.")
return
} catch (let error) {
showErrorAlert(title: "Unable to read zip archive", message: "Unexpected error: \(String(describing: error))")
return
}
for (i, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
let fileBaseName = URL(string: unarchivedFile.fileName)?.deletingPathExtension().lastPathComponent
if let trimmedName = fileBaseName?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmedName.isEmpty {
unarchivedFiles[i].fileName = trimmedName
} else {
unarchivedFiles.remove(at: i)
}
}
if (unarchivedFiles.isEmpty) {
showErrorAlert(title: "No tunnels in zip archive", message: "No .conf tunnel files were found inside the zip archive.")
return
}
guard let tunnelsManager = tunnelsManager else { return }
unarchivedFiles.sort { $0.fileName < $1.fileName }
var lastFileName: String?
var configs: [TunnelConfiguration] = []
for file in unarchivedFiles {
if file.fileName == lastFileName {
continue
}
lastFileName = file.fileName
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
continue
}
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileName) else {
continue
}
configs.append(tunnelConfig)
}
tunnelsManager.addMultiple(tunnelConfigurations: configs) { [weak self] (numberSuccessful) in
if numberSuccessful == unarchivedFiles.count {
return
}
self?.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
message: "Created \(numberSuccessful) of \(unarchivedFiles.count) tunnels from zip archive")
}
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
}
} else {
showErrorAlert(title: "Unable to import tunnel", message: "An error occured when importing the tunnel configuration.")
}
}
}
}
// MARK: UIDocumentPickerDelegate
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach(importFromFile)
}
}
// MARK: QRScanViewControllerDelegate
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { (_, error) in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else {
completionHandler?()
}
}
}
}
// MARK: UITableViewDataSource
extension TunnelsListTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (tunnelsManager?.numberOfTunnels() ?? 0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelsListTableViewCell.id, for: indexPath) as! TunnelsListTableViewCell
if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let s = self, let tunnelsManager = s.tunnelsManager else { return }
if (isOn) {
tunnelsManager.startActivation(of: tunnel) { [weak s] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: s, onPresented: {
DispatchQueue.main.async {
cell.statusSwitch.isOn = false
}
})
}
}
} else {
tunnelsManager.startDeactivation(of: tunnel) { [weak s] error in
s?.showErrorAlert(title: "Deactivation error", message: "Error while bringing down tunnel: \(String(describing: error))")
}
}
}
}
return cell
}
}
// MARK: UITableViewDelegate
extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete", handler: { [weak self] (_, _, completionHandler) in
guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel, completionHandler: { (error) in
if (error != nil) {
ErrorPresenter.showErrorAlert(error: error!, from: self)
completionHandler(false)
} else {
completionHandler(true)
}
})
})
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
// MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerDelegate {
func tunnelAdded(at index: Int) {
tableView?.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
func tunnelModified(at index: Int) {
tableView?.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
func tunnelMoved(at oldIndex: Int, to newIndex: Int) {
tableView?.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
}
func tunnelRemoved(at index: Int) {
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton?.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
}
class TunnelsListTableViewCell: UITableViewCell {
static let id: String = "TunnelsListTableViewCell"
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] (tunnel, _) in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
self?.update(from: tunnel.status)
}
}
}
var onSwitchToggled: ((Bool) -> Void)?
let nameLabel: UILabel
let busyIndicator: UIActivityIndicatorView
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
nameLabel = UILabel()
busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
statusSwitch = UISwitch()
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
busyIndicator.rightAnchor.constraint(equalTo: statusSwitch.leftAnchor, constant: -8)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16),
nameLabel.rightAnchor.constraint(equalTo: busyIndicator.leftAnchor)
])
self.accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.async { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if (status == .inactive || status == .active) {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
class BorderedTextButton: UIView {
let button: UIButton
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
button = UIButton(type: .system)
super.init(frame: CGRect.zero)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: self.centerXAnchor),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor)
])
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
extension UITableViewCell {
static var reuseIdentifier: String {
return NSStringFromClass(self)
}
}
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) {
register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
}
func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
//swiftlint:disable:next force_cast
return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
}
}
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class BorderedTextButton: UIView {
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
override var intrinsicContentSize: CGSize {
let buttonSize = button.intrinsicContentSize
return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
}
var title: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var onTapped: (() -> Void)?
init() {
super.init(frame: CGRect.zero)
layer.borderWidth = 1
layer.cornerRadius = 5
layer.borderColor = button.tintColor.cgColor
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: centerXAnchor),
button.centerYAnchor.constraint(equalTo: centerYAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func buttonTapped() {
onTapped?()
}
}
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class ButtonCell: UITableViewCell {
var buttonText: String {
get { return button.title(for: .normal) ?? "" }
set(value) { button.setTitle(value, for: .normal) }
}
var hasDestructiveAction: Bool {
get { return button.tintColor == .red }
set(value) { button.tintColor = value ? .red : buttonStandardTintColor }
}
var onTapped: (() -> Void)?
let button: UIButton = {
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.titleLabel?.adjustsFontForContentSizeCategory = true
return button
}()
var buttonStandardTintColor: UIColor
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
buttonStandardTintColor = button.tintColor
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
])
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}
@objc func buttonTapped() {
onTapped?()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
buttonText = ""
onTapped = nil
hasDestructiveAction = false
}
}
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class CheckmarkCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
var isChecked: Bool {
didSet {
accessoryType = isChecked ? .checkmark : .none
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
isChecked = false
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
isChecked = false
}
}
@@ -0,0 +1,217 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class KeyValueCell: UITableViewCell {
let keyLabel: UILabel = {
let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true
keyLabel.textColor = .black
keyLabel.textAlignment = .left
return keyLabel
}()
let valueLabelScrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.isDirectionalLockEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
return scrollView
}()
let valueTextField: UITextField = {
let valueTextField = UITextField()
valueTextField.textAlignment = .right
valueTextField.isEnabled = false
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
valueTextField.adjustsFontForContentSizeCategory = true
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
valueTextField.textColor = .gray
return valueTextField
}()
var copyableGesture = true
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
}
var value: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
var placeholderText: String {
get { return valueTextField.placeholder ?? "" }
set(value) { valueTextField.placeholder = value }
}
var keyboardType: UIKeyboardType {
get { return valueTextField.keyboardType }
set(value) { valueTextField.keyboardType = value }
}
var isValueValid = true {
didSet {
if isValueValid {
keyLabel.textColor = .black
} else {
keyLabel.textColor = .red
}
}
}
var isStackedHorizontally = false
var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]()
var onValueChanged: ((String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
private var textFieldValueOnBeginEditing: String = ""
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
])
valueTextField.delegate = self
valueLabelScrollView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.leftAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leftAnchor),
valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
valueTextField.rightAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.rightAnchor),
valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor)
])
let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0)
expandToFitValueLabelConstraint.priority = .defaultLow + 1
expandToFitValueLabelConstraint.isActive = true
contentView.addSubview(valueLabelScrollView)
contentView.addSubview(valueLabelScrollView)
valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueLabelScrollView.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor),
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5)
])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
addGestureRecognizer(gestureRecognizer)
isUserInteractionEnabled = true
configureForContentSize()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureForContentSize() {
var constraints = [NSLayoutConstraint]()
if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
// Stack vertically
if !isStackedVertically {
constraints = [
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.rightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.rightAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leftAnchor.constraint(equalToSystemSpacingAfter: keyLabel.rightAnchor, multiplier: 1),
valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
]
isStackedHorizontally = true
isStackedVertically = false
}
}
if !constraints.isEmpty {
NSLayoutConstraint.deactivate(contentSizeBasedConstraints)
NSLayoutConstraint.activate(constraints)
contentSizeBasedConstraints = constraints
}
}
@objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
if !copyableGesture {
return
}
guard recognizer.state == .recognized else { return }
if let recognizerView = recognizer.view,
let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
let menuController = UIMenuController.shared
menuController.setTargetRect(detailTextLabel?.frame ?? recognizerView.frame, in: detailTextLabel?.superview ?? recognizerSuperView)
menuController.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return (action == #selector(UIResponderStandardEditActions.copy(_:)))
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = valueTextField.text
}
override func prepareForReuse() {
super.prepareForReuse()
copyableGesture = true
placeholderText = ""
isValueValid = true
keyboardType = .default
onValueChanged = nil
onValueBeingEdited = nil
key = ""
value = ""
configureForContentSize()
}
}
extension KeyValueCell: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
textFieldValueOnBeginEditing = textField.text ?? ""
isValueValid = true
}
func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
guard isModified else { return }
onValueChanged?(textField.text ?? "")
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let onValueBeingEdited = onValueBeingEdited {
let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
onValueBeingEdited(modifiedText)
}
return true
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class SwitchCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var isOn: Bool {
get { return switchView.isOn }
set(value) { switchView.isOn = value }
}
var isEnabled: Bool {
get { return switchView.isEnabled }
set(value) {
switchView.isEnabled = value
textLabel?.textColor = value ? .black : .gray
}
}
var onSwitchToggled: ((Bool) -> Void)?
let switchView = UISwitch()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
accessoryView = switchView
switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func switchToggled() {
onSwitchToggled?(switchView.isOn)
}
override func prepareForReuse() {
super.prepareForReuse()
isEnabled = true
message = ""
isOn = false
}
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelEditKeyValueCell: KeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
keyLabel.textAlignment = .right
valueTextField.textAlignment = .left
let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0)
// In case the key doesn't fit into 0.4 * width,
// so set a CR priority > the 0.4-constraint's priority.
widthRatioConstraint.priority = .defaultHigh + 1
widthRatioConstraint.isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
copyableGesture = false
valueTextField.textColor = .black
valueTextField.isEnabled = true
valueLabelScrollView.isScrollEnabled = false
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelListCell: UITableViewCell {
var tunnel: TunnelContainer? {
didSet(value) {
// Bind to the tunnel's name
nameLabel.text = tunnel?.name ?? ""
nameObservervationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
self?.update(from: tunnel.status)
}
}
}
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 busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
let statusSwitch = UISwitch()
private var statusObservervationToken: AnyObject?
private var nameObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(statusSwitch)
statusSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
contentView.rightAnchor.constraint(equalTo: statusSwitch.rightAnchor)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leftAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.rightAnchor, multiplier: 1)
])
contentView.addSubview(nameLabel)
nameLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
nameLabel.leftAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leftAnchor, multiplier: 1),
busyIndicator.leftAnchor.constraint(equalToSystemSpacingAfter: nameLabel.rightAnchor, multiplier: 1),
bottomAnchorConstraint
])
accessoryType = .disclosureIndicator
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
onSwitchToggled?(statusSwitch.isOn)
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak statusSwitch, weak busyIndicator] in
guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
if status == .inactive || status == .active {
busyIndicator.stopAnimating()
} else {
busyIndicator.startAnimating()
}
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class MainViewController: UISplitViewController {
var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController?
init() {
let detailVC = UIViewController()
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController()
let masterNC = UINavigationController(rootViewController: masterVC)
tunnelsListVC = masterVC
super.init(nibName: nil, bundle: nil)
viewControllers = [ masterNC, detailNC ]
// State restoration
restorationIdentifier = "MainVC"
masterNC.restorationIdentifier = "MasterNC"
detailNC.restorationIdentifier = "DetailNC"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
delegate = self
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
preferredDisplayMode = .allVisible
// Create the tunnels manager, and when it's ready, inform tunnelsListVC
TunnelsManager.create { [weak self] result in
guard let self = self else { return }
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let tunnelsManager: TunnelsManager = result.value!
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = self
self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
}
}
}
extension MainViewController: TunnelsManagerActivationDelegate {
func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
// Nothing to do
}
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
ErrorPresenter.showErrorAlert(error: error, from: self)
}
func tunnelActivationSucceeded(tunnel: TunnelContainer) {
// Nothing to do
}
}
extension MainViewController {
func refreshTunnelConnectionStatuses() {
if let tunnelsManager = tunnelsManager {
tunnelsManager.refreshStatuses()
}
}
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
if let self = self {
if animated {
self.showDetailViewController(tunnelDetailNC, sender: self)
} else {
UIView.performWithoutAnimation {
self.showDetailViewController(tunnelDetailNC, sender: self)
}
}
}
}
}
if let tunnelsManager = tunnelsManager {
showTunnelDetailBlock(tunnelsManager)
} else {
onTunnelsManagerReady = showTunnelDetailBlock
}
}
}
extension MainViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController,
collapseSecondary secondaryViewController: UIViewController,
onto primaryViewController: UIViewController) -> Bool {
// On iPhone, if the secondaryVC (detailVC) is just a UIViewController, it indicates that it's empty,
// so just show the primaryVC (masterVC).
let detailVC = (secondaryViewController as? UINavigationController)?.viewControllers.first
let isDetailVCEmpty: Bool
if let detailVC = detailVC {
isDetailVCEmpty = (type(of: detailVC) == UIViewController.self)
} else {
isDetailVCEmpty = true
}
return isDetailVCEmpty
}
}
@@ -2,7 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import AVFoundation
import CoreData
import UIKit
protocol QRScanViewControllerDelegate: class {
@@ -18,13 +17,13 @@ class QRScanViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Scan QR code"
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
title = "Scan QR code"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = UIColor.lightGray
tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center
view.addSubview(tipLabel)
@@ -32,8 +31,8 @@ class QRScanViewController: UIViewController {
NSLayoutConstraint.activate([
tipLabel.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor),
tipLabel.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor),
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32),
])
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
])
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
@@ -102,7 +101,7 @@ class QRScanViewController: UIViewController {
}
}
previewLayer?.frame = self.view.bounds
previewLayer?.frame = view.bounds
}
func scanDidComplete(withCode code: String) {
@@ -114,27 +113,26 @@ class QRScanViewController: UIViewController {
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert)
alert.addTextField(configurationHandler: nil)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { [weak self] _ in
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default, handler: { [weak self] _ in
let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if (title.isEmpty) { return }
})
alert.addAction(UIAlertAction(title: NSLocalizedString("Save", comment: ""), style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
tunnelConfiguration.interface.name = title
if let s = self {
s.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: s) {
s.dismiss(animated: true, completion: nil)
if let self = self {
self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
self.dismiss(animated: true, completion: nil)
}
}
}))
})
present(alert, animated: true)
}
func scanDidEncounterError(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in
alertController.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
})
present(alertController, animated: true)
captureSession = nil
}
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
enum SettingsFields: String {
case iosAppVersion = "WireGuard for iOS"
case goBackendVersion = "WireGuard Go Backend"
case exportZipArchive = "Export zip archive"
case exportLogFile = "Export log file"
}
let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion],
[.exportZipArchive],
[.exportLogFile]
]
let tunnelsManager: TunnelsManager?
var wireguardCaptionedImage: (view: UIView, size: CGSize)?
init(tunnelsManager: TunnelsManager?) {
self.tunnelsManager = tunnelsManager
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self)
tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let logo = tableView.tableFooterView else { return }
let bottomPadding = max(tableView.layoutMargins.bottom, 10)
let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding)
let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height
var height = tableView.estimatedRowHeight * 1.5
var width = height * imageAspectRatio
let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20)
if width > maxWidth {
width = maxWidth
height = width / imageAspectRatio
}
let needsReload = height != logo.frame.height
logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height)
if needsReload {
tableView.tableFooterView = logo
}
}
@objc func doneTapped() {
dismiss(animated: true, completion: nil)
}
func exportConfigurationsAsZipFile(sourceView: UIView) {
guard let tunnelsManager = tunnelsManager else { return }
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
_ = FileManager.deleteFile(at: destinationURL)
let count = tunnelsManager.numberOfTunnels()
let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration() }
ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
self?.present(fileExportVC, animated: true, completion: nil)
}
}
func exportLogForLastActivatedTunnel(sourceView: UIView) {
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
let timeStampString = dateFormatter.string(from: Date())
let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt")
DispatchQueue.global(qos: .userInitiated).async {
if FileManager.default.fileExists(atPath: destinationURL.path) {
let isDeleted = FileManager.deleteFile(at: destinationURL)
if !isDeleted {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The pre-existing log could not be cleared", from: self)
return
}
}
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to determine extension log path", from: self)
return
}
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else {
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "Unable to write logs to file", from: self)
return
}
DispatchQueue.main.async {
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
// popoverPresentationController shall be non-nil on the iPad
activityVC.popoverPresentationController?.sourceView = sourceView
activityVC.popoverPresentationController?.sourceRect = sourceView.bounds
activityVC.completionWithItemsHandler = { _, _, _, _ in
// Remove the exported log file after the activity has completed
_ = FileManager.deleteFile(at: destinationURL)
}
self.present(activityVC, animated: true)
}
}
}
}
// MARK: UITableViewDataSource
extension SettingsTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return settingsFieldsBySection.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return settingsFieldsBySection[section].count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "About"
case 1:
return "Export configurations"
case 2:
return "Tunnel log"
default:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let field = settingsFieldsBySection[indexPath.section][indexPath.row]
if field == .iosAppVersion || field == .goBackendVersion {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.copyableGesture = false
cell.key = field.rawValue
if field == .iosAppVersion {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
cell.value = appVersion
} else if field == .goBackendVersion {
cell.value = WIREGUARD_GO_VERSION
}
return cell
} else if field == .exportZipArchive {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportConfigurationsAsZipFile(sourceView: cell.button)
}
return cell
} else {
assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
}
return cell
}
}
}
@@ -0,0 +1,260 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
private var onDemandStatusObservervationToken: AnyObject?
private var statusObservervationToken: AnyObject?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
onDemandStatusObservervationToken = nil
statusObservervationToken = nil
}
override func viewDidLoad() {
super.viewDidLoad()
title = tunnelViewModel.interfaceData[.name]
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(SwitchCell.self)
tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self)
// State restoration
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
private func loadSections() {
sections.removeAll()
sections.append(.status)
sections.append(.interface)
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.onDemand)
sections.append(.delete)
}
@objc func editTapped() {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
// MARK: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
loadSections()
title = tunnel.name
tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .status:
return 1
case .interface:
return tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields).count
case .peer(let peerData):
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
case .onDemand:
return 1
case .delete:
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .status:
return "Status"
case .interface:
return "Interface"
case .peer:
return "Peer"
case .onDemand:
return "On-Demand Activation"
case .delete:
return nil
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .status:
return statusCell(for: tableView, at: indexPath)
case .interface:
return interfaceCell(for: tableView, at: indexPath)
case .peer(let peer):
return peerCell(for: tableView, at: indexPath, with: peer)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
case .delete:
return deleteConfigurationCell(for: tableView, at: indexPath)
}
}
private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
let text: String
switch status {
case .inactive:
text = "Inactive"
case .activating:
text = "Activating"
case .active:
text = "Active"
case .deactivating:
text = "Deactivating"
case .reasserting:
text = "Reactivating"
case .restarting:
text = "Restarting"
case .waiting:
text = "Waiting"
}
cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
}
cell.isEnabled = status == .active || status == .inactive
}
statusUpdate(cell, tunnel.status)
statusObservervationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return }
statusUpdate(cell, tunnel.status)
}
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
if isOn {
self.tunnelsManager.startActivation(of: self.tunnel)
} else {
self.tunnelsManager.startDeactivation(of: self.tunnel)
}
}
return cell
}
private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: interfaceFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let field = peerData.filterFieldsWithValueOrControl(peerFields: peerFields)[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = peerData[field]
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = "Activate on demand"
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
onDemandStatusObservervationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting())
}
return cell
}
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Delete tunnel"
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this tunnel?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let tunnelsManager = self?.tunnelsManager, let tunnel = self?.tunnel else { return }
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
self?.navigationController?.navigationController?.popToRootViewController(animated: true)
}
}
return cell
}
}
@@ -0,0 +1,484 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
protocol TunnelEditTableViewControllerDelegate: class {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}
// MARK: TunnelEditTableViewController
class TunnelEditTableViewController: UITableViewController {
private enum Section {
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case addPeer
case onDemand
}
weak var delegate: TunnelEditTableViewControllerDelegate?
let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
[.name],
[.privateKey, .publicKey, .generateKeyPair],
[.addresses, .listenPort, .mtu, .dns]
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
.deletePeer
]
let activateOnDemandOptions: [ActivateOnDemandOption] = [
.useOnDemandOverWiFiOrCellular,
.useOnDemandOverWiFiOnly,
.useOnDemandOverCellularOnly
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting
private var sections = [Section]()
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
activateOnDemandSetting = tunnel.activateOnDemandSetting()
super.init(style: .grouped)
loadSections()
}
init(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
// Use this initializer to create a new tunnel.
// If tunnelConfiguration is passed, data will be prepopulated from that configuration.
self.tunnelsManager = tunnelsManager
tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
super.init(style: .grouped)
loadSections()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = tunnel == nil ? "New configuration" : "Edit configuration"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.register(TunnelEditKeyValueCell.self)
tableView.register(TunnelEditEditableKeyValueCell.self)
tableView.register(ButtonCell.self)
tableView.register(SwitchCell.self)
tableView.register(CheckmarkCell.self)
}
private func loadSections() {
sections.removeAll()
interfaceFieldsBySection.forEach { _ in sections.append(.interface) }
tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
sections.append(.addPeer)
sections.append(.onDemand)
}
@objc func saveTapped() {
tableView.endEditing(false)
let tunnelSaveResult = tunnelViewModel.save()
switch tunnelSaveResult {
case .error(let errorMessage):
let erroringConfiguration = (tunnelViewModel.interfaceData.validatedConfiguration == nil) ? "Interface" : "Peer"
ErrorPresenter.showErrorAlert(title: "Invalid \(erroringConfiguration)", message: errorMessage, from: self)
tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration):
if let tunnel = tunnel {
// We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel,
tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
} else {
// We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration,
activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
} else {
let tunnel: TunnelContainer = result.value!
self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel)
}
}
}
}
}
@objc func cancelTapped() {
dismiss(animated: true, completion: nil)
delegate?.tunnelEditingCancelled()
}
}
// MARK: UITableViewDataSource
extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .interface:
return interfaceFieldsBySection[section].count
case .peer(let peerData):
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
return peerFieldsToShow.count
case .addPeer:
return 1
case .onDemand:
if activateOnDemandSetting.isActivateOnDemandEnabled {
return 4
} else {
return 1
}
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .interface:
return section == 0 ? "Interface" : nil
case .peer:
return "Peer"
case .addPeer:
return nil
case .onDemand:
return "On-Demand Activation"
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .interface:
return interfaceFieldCell(for: tableView, at: indexPath)
case .peer(let peerData):
return peerCell(for: tableView, at: indexPath, with: peerData)
case .addPeer:
return addPeerCell(for: tableView, at: indexPath)
case .onDemand:
return onDemandCell(for: tableView, at: indexPath)
}
}
private func interfaceFieldCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = interfaceFieldsBySection[indexPath.section][indexPath.row]
switch field {
case .generateKeyPair:
return generateKeyPairCell(for: tableView, at: indexPath, with: field)
case .publicKey:
return publicKeyCell(for: tableView, at: indexPath, with: field)
default:
return interfaceFieldKeyValueCell(for: tableView, at: indexPath, with: field)
}
}
private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.tunnelViewModel.interfaceData[.privateKey] = Curve25519.generatePrivateKey().base64EncodedString()
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section)
self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .fade)
}
}
return cell
}
private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
cell.value = tunnelViewModel.interfaceData[field]
return cell
}
private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
case .name, .privateKey:
cell.placeholderText = "Required"
cell.keyboardType = .default
case .addresses, .dns:
cell.placeholderText = "Optional"
cell.keyboardType = .numbersAndPunctuation
case .listenPort, .mtu:
cell.placeholderText = "Automatic"
cell.keyboardType = .numberPad
case .publicKey, .generateKeyPair:
cell.keyboardType = .default
}
cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
// Bind values to view model
cell.value = tunnelViewModel.interfaceData[field]
if field == .dns { // While editing DNS, you might directly set exclude private IPs
cell.onValueBeingEdited = { [weak self] value in
self?.tunnelViewModel.interfaceData[field] = value
}
} else {
cell.onValueChanged = { [weak self] value in
self?.tunnelViewModel.interfaceData[field] = value
}
}
// Compute public key live
if field == .privateKey {
cell.onValueBeingEdited = { [weak self] value in
guard let self = self else { return }
self.tunnelViewModel.interfaceData[.privateKey] = value
if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
}
}
}
return cell
}
private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
let field = peerFieldsToShow[indexPath.row]
switch field {
case .deletePeer:
return deletePeerCell(for: tableView, at: indexPath, peerData: peerData, field: field)
case .excludePrivateIPs:
return excludePrivateIPsCell(for: tableView, at: indexPath, peerData: peerData, field: field)
default:
return peerFieldKeyValueCell(for: tableView, at: indexPath, peerData: peerData, field: field)
}
}
private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.rawValue
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
guard let peerData = peerData else { return }
guard let self = self else { return }
self.showConfirmationAlert(message: "Delete this peer?", buttonTitle: "Delete", from: cell) { [weak self] in
guard let self = self else { return }
let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
tableView.performBatchUpdates({
self.tableView.deleteSections(removedSectionIndices, with: .fade)
if shouldShowExcludePrivateIPs {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
self.tableView.insertRows(at: [rowIndexPath], with: .fade)
}
}
})
}
}
return cell
}
private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.rawValue
cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
cell.isOn = peerData.excludePrivateIPsValue
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns])
if let row = self.peerFields.firstIndex(of: .allowedIPs) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
}
}
return cell
}
private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.rawValue
switch field {
case .publicKey:
cell.placeholderText = "Required"
cell.keyboardType = .default
case .preSharedKey, .endpoint:
cell.placeholderText = "Optional"
cell.keyboardType = .default
case .allowedIPs:
cell.placeholderText = "Optional"
cell.keyboardType = .numbersAndPunctuation
case .persistentKeepAlive:
cell.placeholderText = "Off"
cell.keyboardType = .numberPad
case .excludePrivateIPs, .deletePeer:
cell.keyboardType = .default
}
cell.isValueValid = !peerData.fieldsWithError.contains(field)
cell.value = peerData[field]
if field == .allowedIPs {
cell.onValueBeingEdited = { [weak self, weak peerData] value in
guard let self = self, let peerData = peerData else { return }
let oldValue = peerData.shouldAllowExcludePrivateIPsControl
peerData[.allowedIPs] = value
if oldValue != peerData.shouldAllowExcludePrivateIPsControl {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
if peerData.shouldAllowExcludePrivateIPsControl {
self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
} else {
self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
}
}
}
}
} else {
cell.onValueChanged = { [weak peerData] value in
peerData?[field] = value
}
}
return cell
}
private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = "Add peer"
cell.onTapped = { [weak self] in
guard let self = self else { return }
let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
let addedSectionIndices = self.appendEmptyPeer()
tableView.performBatchUpdates({
tableView.insertSections(addedSectionIndices, with: .fade)
if shouldHideExcludePrivateIPs {
if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
self.tableView.deleteRows(at: [rowIndexPath], with: .fade)
}
}
}, completion: nil)
}
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
if isOn {
self.activateOnDemandSetting.isActivateOnDemandEnabled = true
if self.activateOnDemandSetting.activateOnDemandOption == .none {
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
}
self.loadSections()
self.tableView.insertRows(at: indexPaths, with: .fade)
} else {
self.activateOnDemandSetting.isActivateOnDemandEnabled = false
self.loadSections()
self.tableView.deleteRows(at: indexPaths, with: .fade)
}
}
return cell
} else {
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
let rowOption = activateOnDemandOptions[indexPath.row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = (selectedOption == rowOption)
return cell
}
}
func appendEmptyPeer() -> IndexSet {
tunnelViewModel.appendEmptyPeer()
loadSections()
let addedPeerIndex = tunnelViewModel.peersData.count - 1
return IndexSet(integer: interfaceFieldsBySection.count + addedPeerIndex)
}
func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
tunnelViewModel.deletePeer(peer: peer)
loadSections()
return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
}
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
// MARK: UITableViewDelegate
extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section], indexPath.row > 0 {
return indexPath
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch sections[indexPath.section] {
case .onDemand:
let option = activateOnDemandOptions[indexPath.row - 1]
assert(option != .none)
activateOnDemandSetting.activateOnDemandOption = option
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) }
UIView.performWithoutAnimation {
tableView.reloadRows(at: indexPaths, with: .none)
}
default:
assertionFailure()
}
}
}
@@ -0,0 +1,307 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
import UserNotifications
class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
let tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.estimatedRowHeight = 60
tableView.rowHeight = UITableView.automaticDimension
tableView.separatorStyle = .none
tableView.register(TunnelListCell.self)
return tableView
}()
let centeredAddButton: BorderedTextButton = {
let button = BorderedTextButton()
button.title = "Add a tunnel"
button.isHidden = true
return button
}()
let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}()
override func loadView() {
view = UIView()
view.backgroundColor = .white
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: view.rightAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
guard let self = self else { return }
self.addButtonTapped(sender: self.centeredAddButton)
}
busyIndicator.startAnimating()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "WireGuard"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC"
}
func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self
busyIndicator.stopAnimating()
tableView.reloadData()
centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
}
override func viewWillAppear(_: Bool) {
// Remove selection when getting back to the list view on iPhone
if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
}
}
@objc func addButtonTapped(sender: AnyObject) {
guard tunnelsManager != nil else { return }
let alert = UIAlertController(title: "", message: "Add a new WireGuard tunnel", preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: "Create from file or archive", style: .default) { [weak self] _ in
self?.presentViewControllerForFileImport()
}
alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: "Create from QR code", style: .default) { [weak self] _ in
self?.presentViewControllerForScanningQRCode()
}
alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: "Create from scratch", style: .default) { [weak self] _ in
if let self = self, let tunnelsManager = self.tunnelsManager {
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager, tunnelConfiguration: nil)
}
}
alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
alert.addAction(cancelAction)
// popoverPresentationController will be nil on iPhone and non-nil on iPad
if let sender = sender as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sender
} else if let sender = sender as? UIView {
alert.popoverPresentationController?.sourceView = sender
alert.popoverPresentationController?.sourceRect = sender.bounds
}
present(alert, animated: true, completion: nil)
}
@objc func settingsButtonTapped(sender: UIBarButtonItem!) {
guard tunnelsManager != nil else { return }
let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
let settingsNC = UINavigationController(rootViewController: settingsVC)
settingsNC.modalPresentationStyle = .formSheet
present(settingsNC, animated: true)
}
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager, tunnelConfiguration: TunnelConfiguration?) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager, tunnelConfiguration: tunnelConfiguration)
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
present(editNC, animated: true)
}
func presentViewControllerForFileImport() {
let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
filePicker.delegate = self
present(filePicker, animated: true)
}
func presentViewControllerForScanningQRCode() {
let scanQRCodeVC = QRScanViewController()
scanQRCodeVC.delegate = self
let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
scanQRCodeNC.modalPresentationStyle = .fullScreen
present(scanQRCodeNC, animated: true)
}
func importFromFile(url: URL, completionHandler: (() -> Void)?) {
guard let tunnelsManager = tunnelsManager else { return }
if url.pathExtension == "zip" {
ZipImporter.importConfigFiles(from: url) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
let configs = result.value!
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] numberSuccessful in
if numberSuccessful == configs.count {
completionHandler?()
return
}
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
from: self, onPresented: completionHandler)
}
}
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ {
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
if let fileContents = try? String(contentsOf: url),
let tunnelConfiguration = try? WgQuickConfigFileParser.parse(fileContents, name: fileBaseName) {
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { [weak self] result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: self, onPresented: completionHandler)
} else {
completionHandler?()
}
}
} else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
message: "An error occured when importing the tunnel configuration.",
from: self, onPresented: completionHandler)
}
}
}
}
// MARK: UIDocumentPickerDelegate
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach {
importFromFile(url: $0, completionHandler: nil)
}
}
}
// MARK: QRScanViewControllerDelegate
extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
if let error = result.error {
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else {
completionHandler?()
}
}
}
}
// MARK: UITableViewDataSource
extension TunnelsListTableViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (tunnelsManager?.numberOfTunnels() ?? 0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
if let tunnelsManager = tunnelsManager {
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
if isOn {
tunnelsManager.startActivation(of: tunnel)
} else {
tunnelsManager.startDeactivation(of: tunnel)
}
}
}
return cell
}
}
// MARK: UITableViewDelegate
extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc
}
func tableView(_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] _, _, completionHandler in
guard let tunnelsManager = self?.tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row)
tunnelsManager.remove(tunnel: tunnel) { error in
if error != nil {
ErrorPresenter.showErrorAlert(error: error!, from: self)
completionHandler(false)
} else {
completionHandler(true)
}
}
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
}
// MARK: TunnelsManagerDelegate
extension TunnelsListTableViewController: TunnelsManagerListDelegate {
func tunnelAdded(at index: Int) {
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
}
func tunnelModified(at index: Int) {
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
}
func tunnelRemoved(at index: Int) {
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
}
}
-158
View File
@@ -1,158 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Network
import Foundation
class DNSResolver {
let endpoints: [Endpoint?]
let dispatchGroup: DispatchGroup
var dispatchWorkItems: [DispatchWorkItem]
static var cache = NSCache<NSString, NSString>()
init(endpoints: [Endpoint?]) {
self.endpoints = endpoints
self.dispatchWorkItems = []
self.dispatchGroup = DispatchGroup()
}
func resolveWithoutNetworkRequests() -> [Endpoint?]? {
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
for (i, endpoint) in self.endpoints.enumerated() {
guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) {
resolvedEndpoints[i] = endpoint
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
resolvedEndpoints[i] = resolvedEndpointInCache
} else {
return nil
}
}
return resolvedEndpoints
}
func resolve(completionHandler: @escaping ([Endpoint?]?) -> Void) {
let endpoints = self.endpoints
let dispatchGroup = self.dispatchGroup
dispatchWorkItems = []
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
var isResolvedByDNSRequest: [Bool] = Array<Bool>(repeating: false, count: endpoints.count)
for (i, endpoint) in self.endpoints.enumerated() {
guard let endpoint = endpoint else { continue }
if (endpoint.hasHostAsIPAddress()) {
resolvedEndpoints[i] = endpoint
} else if let resolvedEndpointStringInCache = DNSResolver.cache.object(forKey: endpoint.stringRepresentation() as NSString),
let resolvedEndpointInCache = Endpoint(from: resolvedEndpointStringInCache as String) {
resolvedEndpoints[i] = resolvedEndpointInCache
} else {
let workItem = DispatchWorkItem {
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
isResolvedByDNSRequest[i] = true
}
dispatchWorkItems.append(workItem)
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
}
}
dispatchGroup.notify(queue: .main) {
assert(endpoints.count == resolvedEndpoints.count)
for (i, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint, let resolvedEndpoint = resolvedEndpoints[i] else {
completionHandler(nil)
return
}
if (isResolvedByDNSRequest[i]) {
DNSResolver.cache.setObject(resolvedEndpoint.stringRepresentation() as NSString,
forKey: endpoint.stringRepresentation() as NSString)
}
}
let numberOfEndpointsToResolve = endpoints.compactMap { $0 }.count
let numberOfResolvedEndpoints = resolvedEndpoints.compactMap { $0 }.count
if (numberOfResolvedEndpoints < numberOfEndpointsToResolve) {
completionHandler(nil)
} else {
completionHandler(resolvedEndpoints)
}
}
}
func cancel() {
for workItem in dispatchWorkItems {
workItem.cancel()
}
}
deinit {
cancel()
}
}
extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
var hints = addrinfo(
ai_flags: 0,
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
switch (endpoint.host) {
case .name(let name, _):
// The endpoint is a hostname and needs DNS resolution
let returnValue = getaddrinfo(
name.cString(using: .utf8), // Hostname
"\(endpoint.port)".cString(using: .utf8), // Port
&hints,
&resultPointer)
if (returnValue == 0) {
// getaddrinfo succeeded
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN))
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN))
var ipv4AddressString: String?
var ipv6AddressString: String?
while (resultPointer != nil) {
let result = resultPointer!.pointee
resultPointer = result.ai_next
if (result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size) {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
if (inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil) {
ipv4AddressString = String(cString: ipv4Buffer)
// If we found an IPv4 address, we can stop
break
}
} else if (result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size) {
if (ipv6AddressString != nil) {
// If we already have an IPv6 address, we can skip this one
continue
}
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
if (inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil) {
ipv6AddressString = String(cString: ipv6Buffer)
}
}
}
ipv4Buffer.deallocate()
ipv6Buffer.deallocate()
// We prefer an IPv4 address over an IPv6 address
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) {
return Endpoint(host: NWEndpoint.Host.ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6AddressString = ipv6AddressString, let ipv6Address = IPv6Address(ipv6AddressString) {
return Endpoint(host: NWEndpoint.Host.ipv6(ipv6Address), port: endpoint.port)
} else {
return nil
}
} else {
// getaddrinfo failed
return nil
}
default:
// The endpoint is already resolved
return endpoint
}
}
}
@@ -1,178 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
class PacketTunnelOptionsGenerator {
static func generateOptions(from tc: TunnelConfiguration,
withResolvedEndpoints resolvedEndpoints: [Endpoint?]) -> [String: NSObject] {
var options: [String: NSObject] = [:]
// Interface name
options[.interfaceName] = tc.interface.name as NSObject
// WireGuard settings
var wgSettings = ""
let privateKey = tc.interface.privateKey.hexEncodedString()
wgSettings.append("private_key=\(privateKey)\n")
if let listenPort = tc.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n")
}
if (tc.peers.count > 0) {
wgSettings.append("replace_peers=true\n")
}
assert(tc.peers.count == resolvedEndpoints.count)
for (i, peer) in tc.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let preSharedKey = peer.preSharedKey {
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
}
if let endpoint = resolvedEndpoints[i] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
}
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if (!peer.allowedIPs.isEmpty) {
wgSettings.append("replace_allowed_ips=true\n")
for ip in peer.allowedIPs {
wgSettings.append("allowed_ip=\(ip.stringRepresentation())\n")
}
}
}
options[.wireguardSettings] = wgSettings as NSObject
// Remote address
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
var remoteAddress: String = "0.0.0.0"
let endpointsCompact = resolvedEndpoints.compactMap({ $0 })
if endpointsCompact.count == 1 {
switch (endpointsCompact.first!.host) {
case .ipv4(let address):
remoteAddress = "\(address)"
case .ipv6(let address):
remoteAddress = "\(address)"
default:
break
}
}
options[.remoteAddress] = remoteAddress as NSObject
// DNS
options[.dnsServers] = tc.interface.dns.map { $0.stringRepresentation() } as NSObject
// MTU
options[.mtu] = NSNumber(value: tc.interface.mtu ?? 0) // 0 implies auto-MTU
// Addresses from interface addresses
var ipv4Addresses: [String] = []
var ipv4SubnetMasks: [String] = []
var ipv6Addresses: [String] = []
var ipv6NetworkPrefixLengths: [NSNumber] = []
for addressRange in tc.interface.addresses {
if (addressRange.address is IPv4Address) {
ipv4Addresses.append("\(addressRange.address)")
ipv4SubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6Addresses.append("\(addressRange.address)")
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
options[.ipv4Addresses] = ipv4Addresses as NSObject
options[.ipv4SubnetMasks] = ipv4SubnetMasks as NSObject
options[.ipv6Addresses] = ipv6Addresses as NSObject
options[.ipv6NetworkPrefixLengths] = ipv6NetworkPrefixLengths as NSObject
// Included routes from AllowedIPs
var ipv4IncludedRouteAddresses: [String] = []
var ipv4IncludedRouteSubnetMasks: [String] = []
var ipv6IncludedRouteAddresses: [String] = []
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
for peer in tc.peers {
for addressRange in peer.allowedIPs {
if (addressRange.address is IPv4Address) {
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
ipv4IncludedRouteSubnetMasks.append(ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
}
options[.ipv4IncludedRouteAddresses] = ipv4IncludedRouteAddresses as NSObject
options[.ipv4IncludedRouteSubnetMasks] = ipv4IncludedRouteSubnetMasks as NSObject
options[.ipv6IncludedRouteAddresses] = ipv6IncludedRouteAddresses as NSObject
options[.ipv6IncludedRouteNetworkPrefixLengths] = ipv6IncludedRouteNetworkPrefixLengths as NSObject
// Excluded routes from endpoints
var ipv4ExcludedRouteAddresses: [String] = []
var ipv4ExcludedRouteSubnetMasks: [String] = []
var ipv6ExcludedRouteAddresses: [String] = []
var ipv6ExcludedRouteNetworkPrefixLengths: [NSNumber] = []
for endpoint in resolvedEndpoints {
guard let endpoint = endpoint else { continue }
switch (endpoint.host) {
case .ipv4(let address):
ipv4ExcludedRouteAddresses.append("\(address)")
ipv4ExcludedRouteSubnetMasks.append("255.255.255.255") // A single IPv4 address
case .ipv6(let address):
ipv6ExcludedRouteAddresses.append("\(address)")
ipv6ExcludedRouteNetworkPrefixLengths.append(NSNumber(value: UInt8(128))) // A single IPv6 address
default:
fatalError()
}
}
options[.ipv4ExcludedRouteAddresses] = ipv4ExcludedRouteAddresses as NSObject
options[.ipv4ExcludedRouteSubnetMasks] = ipv4ExcludedRouteSubnetMasks as NSObject
options[.ipv6ExcludedRouteAddresses] = ipv6ExcludedRouteAddresses as NSObject
options[.ipv6ExcludedRouteNetworkPrefixLengths] = ipv6ExcludedRouteNetworkPrefixLengths as NSObject
return options
}
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let n: UInt8 = addressRange.networkPrefixLength
assert(n <= 32)
var octets: [UInt8] = [0, 0, 0, 0]
let subnetMask: UInt32 = n > 0 ? ~UInt32(0) << (32 - n) : UInt32(0)
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
return octets.map { String($0) }.joined(separator: ".")
}
}
private extension Data {
func hexEncodedString() -> String {
return self.map { String(format: "%02x", $0) }.joined()
}
}
@@ -1,455 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os.log
protocol TunnelsManagerDelegate: class {
func tunnelAdded(at: Int)
func tunnelModified(at: Int)
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at: Int)
}
enum TunnelActivationError: Error {
case dnsResolutionFailed
case tunnelActivationFailed
case attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: TunnelStatus)
case attemptingActivationWhenTunnelIsNotInactive
case attemptingDeactivationWhenTunnelIsInactive
}
enum TunnelManagementError: Error {
case tunnelAlreadyExistsWithThatName
case tunnelInvalidName
case vpnSystemErrorOnAddTunnel
case vpnSystemErrorOnModifyTunnel
case vpnSystemErrorOnRemoveTunnel
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var delegate: TunnelsManagerDelegate?
private var isAddingTunnel: Bool = false
private var isModifyingTunnel: Bool = false
private var isDeletingTunnel: Bool = false
init(tunnelProviders: [NETunnelProviderManager]) {
self.tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
}
static func create(completionHandler: @escaping (TunnelsManager?) -> Void) {
#if targetEnvironment(simulator)
// NETunnelProviderManager APIs don't work on the simulator
completionHandler(TunnelsManager(tunnelProviders: []))
#else
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
if let error = error {
os_log("Failed to load tunnel provider managers: %{public}@", log: OSLog.default, type: .debug, "\(error)")
return
}
completionHandler(TunnelsManager(tunnelProviders: managers ?? []))
}
#endif
}
func add(tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelContainer?, TunnelManagementError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
return
}
if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(nil, TunnelManagementError.tunnelAlreadyExistsWithThatName)
return
}
isAddingTunnel = true
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isAddingTunnel = false }
guard (error == nil) else {
os_log("Add: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(nil, TunnelManagementError.vpnSystemErrorOnAddTunnel)
return
}
if let s = self {
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
s.tunnels.append(tunnel)
s.tunnels.sort { $0.name < $1.name }
s.delegate?.tunnelAdded(at: s.tunnels.firstIndex(of: tunnel)!)
completionHandler(tunnel, nil)
}
}
}
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler)
}
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) {
guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful)
return
}
let tail = tunnelConfigurations.dropFirst()
self.add(tunnelConfiguration: head) { [weak self, tail] (_, error) in
DispatchQueue.main.async {
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (error == nil ? 1 : 0), completionHandler: completionHandler)
}
}
}
func modify(tunnel: TunnelContainer, with tunnelConfiguration: TunnelConfiguration, completionHandler: @escaping (TunnelManagementError?) -> Void) {
let tunnelName = tunnelConfiguration.interface.name
if tunnelName.isEmpty {
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
return
}
isModifyingTunnel = true
let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = (tunnelName != tunnelProviderManager.localizedDescription)
var oldName: String?
if (isNameChanged) {
if self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(TunnelManagementError.tunnelAlreadyExistsWithThatName)
return
}
oldName = tunnel.name
tunnel.name = tunnelName
}
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
tunnelProviderManager.saveToPreferences { [weak self] (error) in
defer { self?.isModifyingTunnel = false }
guard (error == nil) else {
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelManagementError.vpnSystemErrorOnModifyTunnel)
return
}
if let s = self {
if (isNameChanged) {
let oldIndex = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.sort { $0.name < $1.name }
let newIndex = s.tunnels.firstIndex(of: tunnel)!
s.delegate?.tunnelMoved(at: oldIndex, to: newIndex)
}
s.delegate?.tunnelModified(at: s.tunnels.firstIndex(of: tunnel)!)
if (tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting) {
// Turn off the tunnel, and then turn it back on, so the changes are made effective
tunnel.beginRestart()
}
completionHandler(nil)
}
}
}
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelManagementError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider
isDeletingTunnel = true
tunnelProviderManager.removeFromPreferences { [weak self] (error) in
defer { self?.isDeletingTunnel = false }
guard (error == nil) else {
os_log("Remove: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelManagementError.vpnSystemErrorOnRemoveTunnel)
return
}
if let s = self {
let index = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.remove(at: index)
s.delegate?.tunnelRemoved(at: index)
}
completionHandler(nil)
}
}
func numberOfTunnels() -> Int {
return tunnels.count
}
func tunnel(at index: Int) -> TunnelContainer {
return tunnels[index]
}
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
guard (tunnel.status == .inactive) else {
completionHandler(TunnelActivationError.attemptingActivationWhenTunnelIsNotInactive)
return
}
for t in tunnels {
if t.status != .inactive {
completionHandler(TunnelActivationError.attemptingActivationWhenAnotherTunnelIsBusy(otherTunnelStatus: t.status))
return
}
}
tunnel.startActivation(completionHandler: completionHandler)
}
func startDeactivation(of tunnel: TunnelContainer, completionHandler: @escaping (Error?) -> Void) {
if (tunnel.status == .inactive) {
completionHandler(TunnelActivationError.attemptingDeactivationWhenTunnelIsInactive)
return
}
tunnel.startDeactivation()
}
}
extension NETunnelProviderProtocol {
convenience init?(tunnelConfiguration: TunnelConfiguration) {
assert(!tunnelConfiguration.interface.name.isEmpty)
guard let serializedTunnelConfiguration = try? JSONEncoder().encode(tunnelConfiguration) else { return nil }
self.init()
let appId = Bundle.main.bundleIdentifier!
providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [
"tunnelConfiguration": serializedTunnelConfiguration,
"tunnelConfigurationVersion": 1
]
let endpoints = tunnelConfiguration.peers.compactMap({$0.endpoint})
if endpoints.count == 1 {
serverAddress = endpoints.first!.stringRepresentation()
} else if endpoints.isEmpty {
serverAddress = "Unspecified"
} else {
serverAddress = "Multiple endpoints"
}
username = tunnelConfiguration.interface.name
}
func tunnelConfiguration() -> TunnelConfiguration? {
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return nil }
return try? JSONDecoder().decode(TunnelConfiguration.self, from: serializedTunnelConfiguration)
}
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
fileprivate let tunnelProvider: NETunnelProviderManager
private var statusObservationToken: AnyObject?
private var dnsResolver: DNSResolver?
init(tunnel: NETunnelProviderManager) {
self.name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status)
self.status = status
self.tunnelProvider = tunnel
super.init()
if (status != .inactive) {
startObservingTunnelStatus()
}
}
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
}
fileprivate func startActivation(completionHandler: @escaping (Error?) -> Void) {
assert(status == .inactive || status == .restarting)
assert(self.dnsResolver == nil)
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
// Resolve DNS and start the tunnel
let dnsResolver = DNSResolver(endpoints: endpoints)
let resolvedEndpoints = dnsResolver.resolveWithoutNetworkRequests()
if let resolvedEndpoints = resolvedEndpoints {
// If we don't have to make a DNS network request, we never
// change the status to .resolvingEndpointDomains
startActivation(tunnelConfiguration: tunnelConfiguration,
resolvedEndpoints: resolvedEndpoints,
completionHandler: completionHandler)
} else {
status = .resolvingEndpointDomains
self.dnsResolver = dnsResolver
dnsResolver.resolve { [weak self] resolvedEndpoints in
guard let s = self else { return }
assert(s.status == .resolvingEndpointDomains)
s.dnsResolver = nil
guard let resolvedEndpoints = resolvedEndpoints else {
s.status = .inactive
completionHandler(TunnelActivationError.dnsResolutionFailed)
return
}
s.startActivation(tunnelConfiguration: tunnelConfiguration,
resolvedEndpoints: resolvedEndpoints,
completionHandler: completionHandler)
}
}
}
fileprivate func startActivation(recursionCount: UInt = 0,
lastError: Error? = nil,
tunnelConfiguration: TunnelConfiguration,
resolvedEndpoints: [Endpoint?],
completionHandler: @escaping (Error?) -> Void) {
if (recursionCount >= 8) {
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
completionHandler(TunnelActivationError.tunnelActivationFailed)
return
}
// resolvedEndpoints should contain only IP addresses, not any named endpoints
assert(resolvedEndpoints.allSatisfy { (resolvedEndpoint) in
guard let resolvedEndpoint = resolvedEndpoint else { return true }
switch (resolvedEndpoint.host) {
case .ipv4: return true
case .ipv6: return true
case .name: return false
}
})
os_log("startActivation: Entering", log: OSLog.default, type: .debug)
guard (tunnelProvider.isEnabled) else {
// In case the tunnel had gotten disabled, re-enable and save it,
// then call this function again.
os_log("startActivation: Tunnel is disabled. Re-enabling and saving", log: OSLog.default, type: .info)
tunnelProvider.isEnabled = true
tunnelProvider.saveToPreferences { [weak self] (error) in
if (error != nil) {
os_log("Error saving tunnel after re-enabling: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(error)
return
}
os_log("startActivation: Tunnel saved after re-enabling", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
self?.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
}
return
}
// Start the tunnel
startObservingTunnelStatus()
let session = (tunnelProvider.connection as! NETunnelProviderSession)
do {
os_log("startActivation: Generating options", log: OSLog.default, type: .debug)
let tunnelOptions = PacketTunnelOptionsGenerator.generateOptions(
from: tunnelConfiguration, withResolvedEndpoints: resolvedEndpoints)
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
try session.startTunnel(options: tunnelOptions)
os_log("startActivation: Success", log: OSLog.default, type: .debug)
completionHandler(nil)
} catch (let error) {
os_log("startActivation: Error starting tunnel. Examining error", log: OSLog.default, type: .debug)
guard let vpnError = error as? NEVPNError else {
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(error)
return
}
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(error)
return
}
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
os_log("startActivation: Error says: %{public}@", log: OSLog.default, type: .debug,
vpnError.code == NEVPNError.configurationInvalid ? "Configuration invalid" : "Configuration stale")
os_log("startActivation: Will reload tunnel and then try to start it. ", log: OSLog.default, type: .info)
tunnelProvider.loadFromPreferences { [weak self] (error) in
if (error != nil) {
os_log("Failed to activate tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
self?.status = .inactive
completionHandler(error)
return
}
os_log("startActivation: Tunnel reloaded", log: OSLog.default, type: .info)
os_log("startActivation: Invoking startActivation", log: OSLog.default, type: .debug)
self?.startActivation(recursionCount: recursionCount + 1, lastError: vpnError, tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints, completionHandler: completionHandler)
}
}
}
fileprivate func startDeactivation() {
assert(status == .active)
assert(statusObservationToken != nil)
let session = (tunnelProvider.connection as! NETunnelProviderSession)
session.stopTunnel()
}
fileprivate func beginRestart() {
assert(status == .active || status == .activating || status == .reasserting)
assert(statusObservationToken != nil)
status = .restarting
let session = (tunnelProvider.connection as! NETunnelProviderSession)
session.stopTunnel()
}
private func startObservingTunnelStatus() {
if (statusObservationToken != nil) { return }
let connection = tunnelProvider.connection
statusObservationToken = NotificationCenter.default.addObserver(
forName: .NEVPNStatusDidChange,
object: connection,
queue: nil) { [weak self] (_) in
guard let s = self else { return }
if ((s.status == .restarting) && (connection.status == .disconnected || connection.status == .disconnecting)) {
// Don't change s.status when disconnecting for a restart
if (connection.status == .disconnected) {
self?.startActivation(completionHandler: { _ in })
}
return
}
if (s.status == .resolvingEndpointDomains && connection.status == .disconnected) {
// Don't change to .inactive if we're still resolving endpoints
return
}
s.status = TunnelStatus(from: connection.status)
if (s.status == .inactive) {
s.statusObservationToken = nil
}
}
}
}
@objc enum TunnelStatus: Int {
case inactive
case activating
case active
case deactivating
case reasserting // Not a possible state at present
case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
case resolvingEndpointDomains // DNS resolution in progress
init(from vpnStatus: NEVPNStatus) {
switch (vpnStatus) {
case .connected:
self = .active
case .connecting:
self = .activating
case .disconnected:
self = .inactive
case .disconnecting:
self = .deactivating
case .reasserting:
self = .reasserting
case .invalid:
self = .inactive
}
}
}
@@ -2,3 +2,4 @@
#include "unzip.h"
#include "zip.h"
#include "wireguard-go-version.h"
#include "ringlogger.h"
@@ -6,5 +6,9 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_ID)</string>
</array>
</dict>
</plist>
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error {
typealias AlertText = (title: String, message: String)
var alertText: AlertText { get }
}
+28
View File
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> {
case success(_ value: T)
case failure(_ error: WireGuardAppError)
var value: T? {
switch self {
case .success(let value): return value
case .failure: return nil
}
}
var error: WireGuardAppError? {
switch self {
case .success: return nil
case .failure(let error): return error
}
}
var isSuccess: Bool {
switch self {
case .success: return true
case .failure: return false
}
}
}
+29 -26
View File
@@ -3,10 +3,21 @@
import Foundation
enum ZipArchiveError: Error {
enum ZipArchiveError: WireGuardAppError {
case cantOpenInputZipFile
case cantOpenOutputZipFileForWriting
case badArchive
var alertText: AlertText {
switch self {
case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.")
case .cantOpenOutputZipFileForWriting:
return ("Unable to create zip archive", "Could not open zip file for writing.")
case .badArchive:
return ("Unable to read zip archive", "Bad or corrupt zip archive.")
}
}
}
class ZipArchive {
@@ -28,9 +39,9 @@ class ZipArchive {
zipClose(zipFile, nil)
}
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileName: String, contents: Data)] {
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
var results: [(fileName: String, contents: Data)] = []
var results = [(fileBaseName: String, contents: Data)]()
guard let zipFile = unzOpen64(url.path) else {
throw ZipArchiveError.cantOpenInputZipFile
@@ -38,17 +49,13 @@ class ZipArchive {
defer {
unzClose(zipFile)
}
guard (unzGoToFirstFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzGoToFirstFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
var resultOfGoToNextFile: Int32
repeat {
guard (unzOpenCurrentFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzOpenCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
let bufferSize = 1024
let bufferSize = 16384 // 16 KiB
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
@@ -57,37 +64,33 @@ class ZipArchive {
dataBuffer.deallocate()
}
guard (unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK else { throw ZipArchiveError.badArchive }
if let fileURL = URL(string: String(cString: fileNameBuffer)),
!fileURL.hasDirectoryPath,
requiredFileExtensions.contains(fileURL.pathExtension) {
let lastChar = String(cString: fileNameBuffer).suffix(1)
let isDirectory = (lastChar == "/" || lastChar == "\\")
let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) {
var unzippedData = Data()
var bytesRead: Int32 = 0
repeat {
bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize))
if (bytesRead > 0) {
if bytesRead > 0 {
let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) {
(buf: UnsafeMutablePointer<UInt8>) -> Data in
return Data(bytes: buf, count: Int(bytesRead))
return Data(bytes: $0, count: Int(bytesRead))
}
unzippedData.append(dataRead)
}
} while (bytesRead > 0)
results.append((fileName: fileURL.lastPathComponent, contents: unzippedData))
} while bytesRead > 0
results.append((fileBaseName: fileURL.deletingPathExtension().lastPathComponent, contents: unzippedData))
}
guard (unzCloseCurrentFile(zipFile) == UNZ_OK) else {
throw ZipArchiveError.badArchive
}
guard unzCloseCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
resultOfGoToNextFile = unzGoToNextFile(zipFile)
} while (resultOfGoToNextFile == UNZ_OK)
} while resultOfGoToNextFile == UNZ_OK
if (resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE) {
if resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE {
return results
} else {
throw ZipArchiveError.badArchive
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport
var alertText: AlertText {
return ("Nothing to export", "There are no tunnels to export")
}
}
class ZipExporter {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
guard !tunnelConfigurations.isEmpty else {
completion(ZipExporterError.noTunnelsToExport)
return
}
DispatchQueue.global(qos: .userInitiated).async {
var inputsToArchiver: [(fileName: String, contents: Data)] = []
var lastTunnelName: String = ""
for tunnelConfiguration in tunnelConfigurations {
if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) {
let name = tunnelConfiguration.interface.name
if name.isEmpty || name == lastTunnelName { continue }
inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
lastTunnelName = name
}
}
do {
try ZipArchive.archive(inputs: inputsToArchiver, to: url)
} catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(error) }
return
} catch {
fatalError()
}
DispatchQueue.main.async { completion(nil) }
}
}
}
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive
var alertText: AlertText {
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
}
}
class ZipImporter {
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
var unarchivedFiles: [(fileBaseName: String, contents: Data)]
do {
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
for (index, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
let fileBaseName = unarchivedFile.fileBaseName
let trimmedName = fileBaseName.trimmingCharacters(in: .whitespacesAndNewlines)
if !trimmedName.isEmpty {
unarchivedFiles[index].fileBaseName = trimmedName
} else {
unarchivedFiles.remove(at: index)
}
}
if unarchivedFiles.isEmpty {
throw ZipImporterError.noTunnelsInZipArchive
}
} catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(.failure(error)) }
return
} catch {
fatalError()
}
unarchivedFiles.sort { $0.fileBaseName < $1.fileBaseName }
var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
for (index, file) in unarchivedFiles.enumerated() {
if index > 0 && file == unarchivedFiles[index - 1] {
continue
}
guard let fileContents = String(data: file.contents, encoding: .utf8) else {
continue
}
guard let tunnelConfig = try? WgQuickConfigFileParser.parse(fileContents, name: file.fileBaseName) else {
continue
}
configs[index] = tunnelConfig
}
DispatchQueue.main.async { completion(.success(configs)) }
}
}
}
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Network
import Foundation
enum DNSResolverError: Error {
case dnsResolutionFailed(hostnames: [String])
}
class DNSResolver {
static func isAllEndpointsAlreadyResolved(endpoints: [Endpoint?]) -> Bool {
for endpoint in endpoints {
guard let endpoint = endpoint else { continue }
if !endpoint.hasHostAsIPAddress() {
return false
}
}
return true
}
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
let dispatchGroup: DispatchGroup = DispatchGroup()
if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints
}
var resolvedEndpoints: [Endpoint?] = Array(repeating: nil, count: endpoints.count)
for (index, endpoint) in endpoints.enumerated() {
guard let endpoint = endpoint else { continue }
if endpoint.hasHostAsIPAddress() {
resolvedEndpoints[index] = endpoint
} else {
let workItem = DispatchWorkItem {
resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
}
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
}
}
dispatchGroup.wait() // TODO: Timeout?
var hostnamesWithDnsResolutionFailure = [String]()
assert(endpoints.count == resolvedEndpoints.count)
for tuple in zip(endpoints, resolvedEndpoints) {
let endpoint = tuple.0
let resolvedEndpoint = tuple.1
if let endpoint = endpoint {
if resolvedEndpoint == nil {
// DNS resolution failed
guard let hostname = endpoint.hostname() else { fatalError() }
hostnamesWithDnsResolutionFailure.append(hostname)
}
}
}
if !hostnamesWithDnsResolutionFailure.isEmpty {
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
}
return resolvedEndpoints
}
}
extension DNSResolver {
// Based on DNS resolution code by Jason Donenfeld <jason@zx2c4.com>
// in parse_endpoint() in src/tools/config.c in the WireGuard codebase
//swiftlint:disable:next cyclomatic_complexity
private static func resolveSync(endpoint: Endpoint) -> Endpoint? {
switch endpoint.host {
case .name(let name, _):
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
// The endpoint is a hostname and needs DNS resolution
if addressInfo(for: name, port: endpoint.port, resultPointer: &resultPointer) == 0 {
// getaddrinfo succeeded
let ipv4Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET_ADDRSTRLEN))
let ipv6Buffer = UnsafeMutablePointer<Int8>.allocate(capacity: Int(INET6_ADDRSTRLEN))
var ipv4AddressString: String?
var ipv6AddressString: String?
while resultPointer != nil {
let result = resultPointer!.pointee
resultPointer = result.ai_next
if result.ai_family == AF_INET && result.ai_addrlen == MemoryLayout<sockaddr_in>.size {
var sa4 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in.self).pointee
if inet_ntop(result.ai_family, &sa4.sin_addr, ipv4Buffer, socklen_t(INET_ADDRSTRLEN)) != nil {
ipv4AddressString = String(cString: ipv4Buffer)
// If we found an IPv4 address, we can stop
break
}
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
if ipv6AddressString != nil {
// If we already have an IPv6 address, we can skip this one
continue
}
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
if inet_ntop(result.ai_family, &sa6.sin6_addr, ipv6Buffer, socklen_t(INET6_ADDRSTRLEN)) != nil {
ipv6AddressString = String(cString: ipv6Buffer)
}
}
}
ipv4Buffer.deallocate()
ipv6Buffer.deallocate()
// We prefer an IPv4 address over an IPv6 address
if let ipv4AddressString = ipv4AddressString, let ipv4Address = IPv4Address(ipv4AddressString) {
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6AddressString = ipv6AddressString, let ipv6Address = IPv6Address(ipv6AddressString) {
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
return nil
}
} else {
// getaddrinfo failed
return nil
}
default:
// The endpoint is already resolved
return endpoint
}
}
private static func addressInfo(for name: String, port: NWEndpoint.Port, resultPointer: inout UnsafeMutablePointer<addrinfo>?) -> Int32 {
var hints = addrinfo(
ai_flags: 0,
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM, // WireGuard is UDP-only
ai_protocol: IPPROTO_UDP, // WireGuard is UDP-only
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
return getaddrinfo(
name.cString(using: .utf8), // Hostname
"\(port)".cString(using: .utf8), // Port
&hints,
&resultPointer)
}
}
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
class ErrorNotifier {
let activationAttemptId: String?
weak var tunnelProvider: NEPacketTunnelProvider?
init(activationAttemptId: String?, tunnelProvider: NEPacketTunnelProvider) {
self.activationAttemptId = activationAttemptId
self.tunnelProvider = tunnelProvider
ErrorNotifier.removeLastErrorFile()
}
func errorMessage(for error: PacketTunnelProviderError) -> (String, String)? {
switch error {
case .savedProtocolConfigurationIsInvalid:
return ("Activation failure", "Could not retrieve tunnel information from the saved configuration")
case .dnsResolutionFailure:
return ("DNS resolution failure", "One or more endpoint domains could not be resolved")
case .couldNotStartWireGuard:
return ("Activation failure", "WireGuard backend could not be started")
case .coultNotSetNetworkSettings:
return ("Activation failure", "Error applying network settings on the tunnel")
}
}
func notify(_ error: PacketTunnelProviderError) {
guard let (title, message) = errorMessage(for: error) else { return }
if let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path {
// The tunnel was started from the app
let errorMessageData = "\(activationAttemptId)\n\(title)\n\(message)".data(using: .utf8)
FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
} else {
// The tunnel was probably started from iOS Settings app
if let tunnelProvider = self.tunnelProvider {
// displayMessage() is deprecated, but there's no better alternative if invoked from iOS Settings
tunnelProvider.displayMessage("\(title): \(message)") { _ in }
}
}
}
static func removeLastErrorFile() {
if let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL {
_ = FileManager.deleteFile(at: lastErrorFileURL)
}
}
}
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -27,5 +29,7 @@
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).PacketTunnelProvider</string>
</dict>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID)</string>
</dict>
</plist>
@@ -1,11 +1,14 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
import os.log
enum PacketTunnelProviderError: Error {
case invalidOptions
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure(hostnames: [String])
case couldNotStartWireGuard
case coultNotSetNetworkSettings
}
@@ -17,115 +20,104 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var wgHandle: Int32?
private var networkMonitor: NWPathMonitor?
// MARK: NEPacketTunnelProvider
/// Begin the process of establishing the tunnel.
override func startTunnel(options: [String: NSObject]?,
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
os_log("Starting tunnel", log: OSLog.default, type: .info)
deinit {
networkMonitor?.cancel()
}
guard let options = options else {
os_log("Starting tunnel failed: No options passed. Possible connection request from preferences", log: OSLog.default, type: .error)
// displayMessage is deprecated API
displayMessage("Please use the WireGuard app to start WireGuard tunnels") { (_) in
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
}
return
}
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
guard let interfaceName = options[.interfaceName] as? String,
let wireguardSettings = options[.wireguardSettings] as? String,
let remoteAddress = options[.remoteAddress] as? String,
let dnsServers = options[.dnsServers] as? [String],
let mtu = options[.mtu] as? NSNumber,
let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId, tunnelProvider: self)
// IPv4 settings
let ipv4Addresses = options[.ipv4Addresses] as? [String],
let ipv4SubnetMasks = options[.ipv4SubnetMasks] as? [String],
let ipv4IncludedRouteAddresses = options[.ipv4IncludedRouteAddresses] as? [String],
let ipv4IncludedRouteSubnetMasks = options[.ipv4IncludedRouteSubnetMasks] as? [String],
let ipv4ExcludedRouteAddresses = options[.ipv4ExcludedRouteAddresses] as? [String],
let ipv4ExcludedRouteSubnetMasks = options[.ipv4ExcludedRouteSubnetMasks] as? [String],
// IPv6 settings
let ipv6Addresses = options[.ipv6Addresses] as? [String],
let ipv6NetworkPrefixLengths = options[.ipv6NetworkPrefixLengths] as? [NSNumber],
let ipv6IncludedRouteAddresses = options[.ipv6IncludedRouteAddresses] as? [String],
let ipv6IncludedRouteNetworkPrefixLengths = options[.ipv6IncludedRouteNetworkPrefixLengths] as? [NSNumber],
let ipv6ExcludedRouteAddresses = options[.ipv6ExcludedRouteAddresses] as? [String],
let ipv6ExcludedRouteNetworkPrefixLengths = options[.ipv6ExcludedRouteNetworkPrefixLengths] as? [NSNumber]
else {
os_log("Starting tunnel failed: Invalid options passed", log: OSLog.default, type: .error)
startTunnelCompletionHandler(PacketTunnelProviderError.invalidOptions)
guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
startTunnel(with: tunnelConfiguration, errorNotifier: errorNotifier, completionHandler: startTunnelCompletionHandler)
}
//swiftlint:disable:next function_body_length
func startTunnel(with tunnelConfiguration: TunnelConfiguration, errorNotifier: ErrorNotifier, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
configureLogger()
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
if fd < 0 {
os_log("Starting tunnel failed: Could not determine file descriptor", log: OSLog.default, type: .error)
wg_log(.info, message: "Starting tunnel '\(tunnelConfiguration.interface.name)'")
let endpoints = tunnelConfiguration.peers.map { $0.endpoint }
var resolvedEndpoints = [Endpoint?]()
do {
resolvedEndpoints = try DNSResolver.resolveSync(endpoints: endpoints)
} catch DNSResolverError.dnsResolutionFailed(let hostnames) {
wg_log(.error, staticMessage: "Starting tunnel failed: DNS resolution failure")
wg_log(.error, message: "Hostnames for which DNS resolution failed: \(hostnames.joined(separator: ", "))")
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
return
} catch {
// There can be no other errors from DNSResolver.resolveSync()
fatalError()
}
assert(endpoints.count == resolvedEndpoints.count)
// Setup packetTunnelSettingsGenerator
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
// Bring up wireguard-go backend
let fileDescriptor = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32 //swiftlint:disable:this force_cast
if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
let handle = connect(interfaceName: interfaceName, settings: wireguardSettings, fd: fd)
let wireguardSettings = packetTunnelSettingsGenerator.uapiConfiguration()
var handle: Int32 = -1
networkMonitor = NWPathMonitor()
networkMonitor?.pathUpdateHandler = { path in
guard handle >= 0 else { return }
if path.status == .satisfied {
wg_log(.debug, message: "Network change detected, re-establishing sockets and IPs: \(path.availableInterfaces)")
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: wgGetListenPort(handle))
let err = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
if err == -EADDRINUSE {
let endpointString = packetTunnelSettingsGenerator.endpointUapiConfiguration(currentListenPort: 0)
_ = withStringsAsGoStrings(endpointString, call: { return wgSetConfig(handle, $0.0) })
}
}
}
networkMonitor?.start(queue: DispatchQueue(label: "NetworkMonitor"))
handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fileDescriptor: fileDescriptor)
if handle < 0 {
os_log("Starting tunnel failed: Could not start WireGuard", log: OSLog.default, type: .error)
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
wgHandle = handle
// Network settings
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
// Apply network settings
// IPv4 settings
let ipv4Settings = NEIPv4Settings(addresses: ipv4Addresses, subnetMasks: ipv4SubnetMasks)
assert(ipv4IncludedRouteAddresses.count == ipv4IncludedRouteSubnetMasks.count)
ipv4Settings.includedRoutes = zip(ipv4IncludedRouteAddresses, ipv4IncludedRouteSubnetMasks).map {
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
}
assert(ipv4ExcludedRouteAddresses.count == ipv4ExcludedRouteSubnetMasks.count)
ipv4Settings.excludedRoutes = zip(ipv4ExcludedRouteAddresses, ipv4ExcludedRouteSubnetMasks).map {
NEIPv4Route(destinationAddress: $0.0, subnetMask: $0.1)
}
networkSettings.ipv4Settings = ipv4Settings
// IPv6 settings
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
* very bad, if various network parameters were actually relying on that subnet being
* intentionally small. TODO: talk about this with upstream iOS devs.
*/
let ipv6Settings = NEIPv6Settings(addresses: ipv6Addresses, networkPrefixLengths: ipv6NetworkPrefixLengths.map { NSNumber(value: min(120, $0.intValue)) })
assert(ipv6IncludedRouteAddresses.count == ipv6IncludedRouteNetworkPrefixLengths.count)
ipv6Settings.includedRoutes = zip(ipv6IncludedRouteAddresses, ipv6IncludedRouteNetworkPrefixLengths).map {
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
}
assert(ipv6ExcludedRouteAddresses.count == ipv6ExcludedRouteNetworkPrefixLengths.count)
ipv6Settings.excludedRoutes = zip(ipv6ExcludedRouteAddresses, ipv6ExcludedRouteNetworkPrefixLengths).map {
NEIPv6Route(destinationAddress: $0.0, networkPrefixLength: $0.1)
}
networkSettings.ipv6Settings = ipv6Settings
// DNS
networkSettings.dnsSettings = NEDNSSettings(servers: dnsServers)
// MTU
if (mtu == 0) {
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
networkSettings.tunnelOverheadBytes = 80
} else {
networkSettings.mtu = mtu
}
setTunnelNetworkSettings(networkSettings) { (error) in
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
setTunnelNetworkSettings(networkSettings) { error in
if let error = error {
os_log("Starting tunnel failed: Error setting network settings: %s", log: OSLog.default, type: .error, error.localizedDescription)
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
} else {
startTunnelCompletionHandler(nil /* No errors */)
@@ -135,7 +127,12 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
/// Begin the process of stopping the tunnel.
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
os_log("Stopping tunnel", log: OSLog.default, type: .info)
networkMonitor?.cancel()
networkMonitor = nil
ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle {
wgTurnOff(handle)
}
@@ -143,7 +140,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
private func configureLogger() {
wgSetLogger { (level, msgCStr) in
Logger.configureGlobal(withFilePath: FileManager.networkExtensionLogFileURL?.path)
wgSetLogger { level, msgC in
guard let msgC = msgC else { return }
let logType: OSLogType
switch level {
case 0:
@@ -155,24 +154,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
default:
logType = .default
}
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
os_log("%{public}s", log: OSLog.default, type: logType, msg)
wg_log(logType, message: String(cString: msgC))
}
}
private func connect(interfaceName: String, settings: String, fd: Int32) -> Int32 { // swiftlint:disable:this cyclomatic_complexity
return withStringsAsGoStrings(interfaceName, settings) { (nameGoStr, settingsGoStr) -> Int32 in
return wgTurnOn(nameGoStr, settingsGoStr, fd)
}
private func connect(interfaceName: String, settings: String, fileDescriptor: Int32) -> Int32 {
return withStringsAsGoStrings(interfaceName, settings) { return wgTurnOn($0.0, $0.1, fileDescriptor) }
}
}
private func withStringsAsGoStrings<R>(_ str1: String, _ str2: String, closure: (gostring_t, gostring_t) -> R) -> R {
return str1.withCString { (s1cStr) -> R in
let gstr1 = gostring_t(p: s1cStr, n: str1.utf8.count)
return str2.withCString { (s2cStr) -> R in
let gstr2 = gostring_t(p: s2cStr, n: str2.utf8.count)
return closure(gstr1, gstr2)
}
// swiftlint:disable:next large_tuple identifier_name
func withStringsAsGoStrings<R>(_ s1: String, _ s2: String? = nil, _ s3: String? = nil, _ s4: String? = nil, call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
// swiftlint:disable:next large_tuple identifier_name
func helper(_ p1: UnsafePointer<Int8>?, _ p2: UnsafePointer<Int8>?, _ p3: UnsafePointer<Int8>?, _ p4: UnsafePointer<Int8>?, _ call: ((gostring_t, gostring_t, gostring_t, gostring_t)) -> R) -> R {
return call((gostring_t(p: p1, n: s1.utf8.count), gostring_t(p: p2, n: s2?.utf8.count ?? 0), gostring_t(p: p3, n: s3?.utf8.count ?? 0), gostring_t(p: p4, n: s4?.utf8.count ?? 0)))
}
return helper(s1, s2, s3, s4, call)
}
@@ -0,0 +1,182 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
class PacketTunnelSettingsGenerator {
let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?]
init(tunnelConfiguration: TunnelConfiguration, resolvedEndpoints: [Endpoint?]) {
self.tunnelConfiguration = tunnelConfiguration
self.resolvedEndpoints = resolvedEndpoints
}
func endpointUapiConfiguration(currentListenPort: UInt16) -> String {
var wgSettings = "listen_port=\(tunnelConfiguration.interface.listenPort ?? currentListenPort)\n"
for (index, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let endpoint = resolvedEndpoints[index] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
}
}
return wgSettings
}
func uapiConfiguration() -> String {
var wgSettings = ""
let privateKey = tunnelConfiguration.interface.privateKey.hexEncodedString()
wgSettings.append("private_key=\(privateKey)\n")
if let listenPort = tunnelConfiguration.interface.listenPort {
wgSettings.append("listen_port=\(listenPort)\n")
}
if tunnelConfiguration.peers.count > 0 {
wgSettings.append("replace_peers=true\n")
}
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (index, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let preSharedKey = peer.preSharedKey {
wgSettings.append("preshared_key=\(preSharedKey.hexEncodedString())\n")
}
if let endpoint = resolvedEndpoints[index] {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
}
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n")
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation())\n") }
}
}
return wgSettings
}
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
var remoteAddress = "0.0.0.0"
let endpointsCompact = resolvedEndpoints.compactMap { $0 }
if endpointsCompact.count == 1 {
switch endpointsCompact.first!.host {
case .ipv4(let address):
remoteAddress = "\(address)"
case .ipv6(let address):
remoteAddress = "\(address)"
default:
break
}
}
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() }
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
networkSettings.dnsSettings = dnsSettings
let mtu = tunnelConfiguration.interface.mtu ?? 0
if mtu == 0 {
// 0 imples automatic MTU, where we set overhead as 80 bytes, which is the worst case for WireGuard
networkSettings.tunnelOverheadBytes = 80
} else {
networkSettings.mtu = NSNumber(value: mtu)
}
let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
let (ipv4ExcludedRoutes, ipv6ExcludedRoutes) = excludedRoutes()
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes
ipv4Settings.excludedRoutes = ipv4ExcludedRoutes
networkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
ipv6Settings.excludedRoutes = ipv6ExcludedRoutes
networkSettings.ipv6Settings = ipv6Settings
return networkSettings
}
private func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let length: UInt8 = addressRange.networkPrefixLength
assert(length <= 32)
var octets: [UInt8] = [0, 0, 0, 0]
let subnetMask: UInt32 = length > 0 ? ~UInt32(0) << (32 - length) : UInt32(0)
octets[0] = UInt8(truncatingIfNeeded: subnetMask >> 24)
octets[1] = UInt8(truncatingIfNeeded: subnetMask >> 16)
octets[2] = UInt8(truncatingIfNeeded: subnetMask >> 8)
octets[3] = UInt8(truncatingIfNeeded: subnetMask)
return octets.map { String($0) }.joined(separator: ".")
}
private func routes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4Routes = [NEIPv4Route]()
var ipv6Routes = [NEIPv6Route]()
for addressRange in tunnelConfiguration.interface.addresses {
if addressRange.address is IPv4Address {
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
/* Big fat ugly hack for broken iOS networking stack: the smallest prefix that will have
* any effect on iOS is a /120, so we clamp everything above to /120. This is potentially
* very bad, if various network parameters were actually relying on that subnet being
* intentionally small. TODO: talk about this with upstream iOS devs.
*/
ipv6Routes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: min(120, addressRange.networkPrefixLength))))
}
}
return (ipv4Routes, ipv6Routes)
}
private func includedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4IncludedRoutes = [NEIPv4Route]()
var ipv6IncludedRoutes = [NEIPv6Route]()
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if addressRange.address is IPv4Address {
ipv4IncludedRoutes.append(NEIPv4Route(destinationAddress: "\(addressRange.address)", subnetMask: ipv4SubnetMaskString(of: addressRange)))
} else if addressRange.address is IPv6Address {
ipv6IncludedRoutes.append(NEIPv6Route(destinationAddress: "\(addressRange.address)", networkPrefixLength: NSNumber(value: addressRange.networkPrefixLength)))
}
}
}
return (ipv4IncludedRoutes, ipv6IncludedRoutes)
}
private func excludedRoutes() -> ([NEIPv4Route], [NEIPv6Route]) {
var ipv4ExcludedRoutes = [NEIPv4Route]()
var ipv6ExcludedRoutes = [NEIPv6Route]()
for endpoint in resolvedEndpoints {
guard let endpoint = endpoint else { continue }
switch endpoint.host {
case .ipv4(let address):
ipv4ExcludedRoutes.append(NEIPv4Route(destinationAddress: "\(address)", subnetMask: "255.255.255.255"))
case .ipv6(let address):
ipv6ExcludedRoutes.append(NEIPv6Route(destinationAddress: "\(address)", networkPrefixLength: NSNumber(value: UInt8(128))))
default:
fatalError()
}
}
return (ipv4ExcludedRoutes, ipv6ExcludedRoutes)
}
}
private extension Data {
func hexEncodedString() -> String {
return self.map { String(format: "%02x", $0) }.joined()
}
}
@@ -1 +1,3 @@
#include "../../wireguard-go-bridge/wireguard.h"
#include "wireguard-go-version.h"
#include "ringlogger.h"
@@ -6,5 +6,9 @@
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_ID)</string>
</array>
</dict>
</plist>
+24
View File
@@ -127,6 +127,30 @@ func wgTurnOff(tunnelHandle int32) {
device.Close()
}
//export wgSetConfig
func wgSetConfig(tunnelHandle int32, settings string) int64 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return 0
}
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(settings)), bufio.NewWriter(ioutil.Discard))
err := ipcSetOperation(device, bufferedSettings)
if err != nil {
device.log.Error.Println(err)
return err.Code
}
return 0
}
//export wgGetListenPort
func wgGetListenPort(tunnelHandle int32) uint16 {
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return 0
}
return device.net.port
}
//export wgVersion
func wgVersion() *C.char {
return versionString
+2
View File
@@ -14,6 +14,8 @@ typedef void(*logger_fn_t)(int level, const char *msg);
extern void wgSetLogger(logger_fn_t logger_fn);
extern int wgTurnOn(gostring_t ifname, gostring_t settings, int32_t tun_fd);
extern void wgTurnOff(int handle);
extern int64_t wgSetConfig(int handle, gostring_t settings);
extern uint16_t wgGetListenPort(int handle);
extern char *wgVersion();
#endif