Compare commits

...

147 Commits

Author SHA1 Message Date
Jason A. Donenfeld 52c59704de Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-28 19:43:11 +01:00
Jason A. Donenfeld 0b828f9b96 Rework DNS and routes in network extension
The DNS resolver prior had useless comments, awful nesting, converted
bytes into strings and back into bytes, and generally made no sense.
That's been rewritten now.

But more fundumentally, this commit made the DNS resolver actually
accomplish its objective, by passing AI_ALL to it. It turns out, though,
that the Go library isn't actually using GAI in the way we need for
parsing IP addresses, so we actually need to do another round, this time
with hints flag as zero, so that we get the DNS64 address.

Additionally, since we're now binding sockets to interfaces, we can
entirely remove the excludedRoutes logic.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-28 19:38:03 +01:00
Jason A. Donenfeld 51a3e5c0b4 Version bump
A Christmas Special, for TestFlight, and possibly for release if things
go well there.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-26 01:41:22 +01:00
Jason A. Donenfeld c9c343cde2 NetworkExtension: rescope socket instead of tearing down socket
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-26 01:17:55 +01:00
Roopesh Chander c563a24348 minizip: Remove zip encryption code
We can now remove -DNOCRYPT cflag while compiling

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-25 12:48:04 +05:30
Roopesh Chander 808852c547 Tunnel edit: Fix crash
This fixes a crash that happens when you:

1. Scroll to the end of the Edit screen
2. Delete a peer
3. Toggle the Activate On Demand switch

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-24 13:01:21 +05:30
Eric Kuck 035055ef0a SwitchCell nits
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 15:32:29 -06:00
Eric Kuck 508ba44576 Fix typo for simulator builds
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 14:32:39 -06:00
Roopesh Chander 999b761ed0 Remove more comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-23 00:31:44 +05:30
Jason A. Donenfeld 129f94dccd Rely on availability of fd only after setting network settings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 18:29:54 +01:00
Jason A. Donenfeld aaee3c5cbe Bump to latest wireguard-go release
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 17:38:25 +01:00
Jason A. Donenfeld dddbf3b370 Retain aggressive socket reestablishment for now
This can be reverted once we've done more testing.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 15:45:09 +01:00
Roopesh Chander d29f47fc9b Don't set username on NETunnelProviderProtocol
The username corresponds to the Account field in iOS system VPN UI,
but if we don't set it, the field is not shown, so setting it isn't
really required.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 16:13:31 +05:30
Roopesh Chander e6e1795d08 TunnelErrors: Add alert text for PacketTunnelProviderError
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 16:05:43 +05:30
Roopesh Chander fd29cf3402 TunnelStatus: Absorb NEVPNStatus+CustomStringConvertible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 15:46:28 +05:30
Jason A. Donenfeld 56ad5f74e9 Also refresh status
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 07:07:53 +01:00
Jason A. Donenfeld 49bf55021f Reassign tunnelProvider if it changes from outside the app
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 06:56:12 +01:00
Eric Kuck 0bec5b04b0 All models now Equatable
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 22:57:17 -06:00
Jason A. Donenfeld d36e7e27ff Clean up trailing whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 05:30:35 +01:00
Jason A. Donenfeld b0b6866c51 Do not crash if we can't get socket.fileDescriptor
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 05:13:04 +01:00
Eric Kuck 9098cd1161 Removing a tunnel from iOS's settings is now immediately reflected in app
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 21:59:43 -06:00
Jason A. Donenfeld 8365adf435 Localize remaining strings in network extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 03:42:01 +01:00
Eric Kuck 9d9859248e RTL support
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 20:37:22 -06:00
Eric Kuck f7e9f4d631 Strongly recommended now appears as placeholder for DNS when needed
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 19:52:51 -06:00
Jason A. Donenfeld f2000aa1da Combine double log invocations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 02:21:07 +01:00
Jason A. Donenfeld 82de3e3090 Bump go bridge
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:58:02 +01:00
Jason A. Donenfeld 41a4c6362a Attempt to strongly recommend things
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:55:42 +01:00
Jason A. Donenfeld aede9f6e45 Move model helpers to model directory
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:36:42 +01:00
Eric Kuck 1eeed89174 Fixes mock tunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 18:35:01 -06:00
Jason A. Donenfeld c1c5f7a7c7 Do not set copyable back to true on reuse
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:31:59 +01:00
Jason A. Donenfeld 4ed646973e Move name from interface to tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 00:28:18 +01:00
Jason A. Donenfeld 9295895e3a Fix paren typo
"I am very anti-paren." --Eric

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 23:45:20 +01:00
Jason A. Donenfeld 7b9d4cb9e3 Nuke trailing spaces
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 23:34:56 +01:00
Eric Kuck 1fecd8eb6c providerConfiguration is now a WgQuickConfig
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:32:08 -06:00
Jason A. Donenfeld accf60b82f Do not require NetworkExtension to know its own name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 22:05:47 +01:00
Eric Kuck f6af9d9ffb All migration stuff moved to one gross file
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 12:51:14 -06:00
Jason A. Donenfeld 78b38a4eba Simplify versioning of stored data
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:58:06 +01:00
Jason A. Donenfeld ec031b1f19 Get rid of superflous isActivateOnDemandEnabled key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:50:32 +01:00
Eric Kuck 8553723e04 Updated NETunnelProvider save format
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:42:16 +01:00
Jason A. Donenfeld 38445114e0 NE: simplify logic
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 15:56:03 +01:00
Roopesh Chander a21c569e9f NE: Simplify DNS resolution
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 19:24:22 +05:30
Roopesh Chander 0552d75aa1 Localize all the things
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 18:34:09 +05:30
Roopesh Chander e47a8232d8 Tunnel detail: iPad: Handle deletion of tunnel correctly
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 18:02:18 +05:30
Roopesh Chander f818cdd963 NE: Update listen port only when first interface changes
When handling network path changes, change the listen port
only when the first interface has changed.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 17:32:44 +05:30
Roopesh Chander 28ce4d5164 NE: Change handling of bad domain names and Activate On Demand
The solution implemented in commit b8c331c causes the tunnel to
remain in 'Activating' state, without the ability to cancel that.

So, in this commit, instead of retrying DNS silently on
Activated-On-Demand tunnels, we fail the startTunnel() silently.

To summarize, if activate-on-demand is on:
- If started from the WireGuard app, show error using lastErrorFile
mechanism, suggesting a way to turn off Activate On Demand
- If not started from WireGuard app, don't call displayMessage()
(don't show error to user) and silently fail starting the tunnel

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 15:52:47 +05:30
Eric Kuck c2131cb757 Added missing param in MockTunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 13:51:44 -06:00
Jason A. Donenfeld b23578dc7c wireguard-go-bridge: SDK_DIR is not defined for simulator
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-20 20:47:49 +01:00
Eric Kuck a89ad95901 Enabled more swiftlint rules
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 11:22:37 -06:00
Eric Kuck 5618c465a2 Added a String->[String] helper
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 10:46:26 -06:00
Roopesh Chander de08978a80 TunnelErrors: Remove unused error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-20 19:44:57 +05:30
Roopesh Chander 9268c0c4bc Tunnel edit: init() need not take a tunnelConfiguration argument
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander 5c501ac9a6 NE: Log whether tunnel was activated from the app or not
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander 35450bf407 Remove non-helpful comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander f93c9797ea Tunnel edit: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander bba6d2f919 TunnelsManager: If only Activate On Demand has changed, don't restart tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
Roopesh Chander fa51e3f1d1 NE: Handle bad domain names and Activate On Demand
This combination causes iOS to keep trying to bring up the tunnel,
leading to a lot of displayMessage() alerts.

In this fix, if we get a DNS resolution error in an Activate On Demand
enabled tunnel, we silently retry 9 times (with a 4-second delay before
each retry) and then show the displayMessage() alert.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 15:38:00 +05:30
Roopesh Chander 04a8c2ff5a NE: No need for two startTunnel() methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 13:10:42 +05:30
Roopesh Chander 4e516d6769 TunnelsManager: Handle waiting on a stale tunnel
If we have a stale tunnel on which we don't get status updates we rely
on a timer to update the status (see commit 34a7e5b).  Previously, if
the user tries to activate another tunnel, that resulted in both tunnels
waiting indefinitely. This commit fixes that.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 12:48:10 +05:30
Roopesh Chander fab7af6f38 Remove buttons and text from LaunchScreen.storyboard
With state restoration, we're not guaranteed that the
list view will get shown immediately after the launch screen.
So, generalize the launch screen as much as possible.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 23:00:27 +05:30
Roopesh Chander 3ae9fb538d s/Observervation/Observation/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 23:00:27 +05:30
Roopesh Chander 78eaab8b5b Tunnel detail: Update restorationIdentifier when tunnel name changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 19:27:31 +05:30
Roopesh Chander 20f8abdf04 TunnelsManager: Add periods to end the system error messages
Because they can be part of a multi-sentence message when displayed
in the alert.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 15:47:20 +05:30
Roopesh Chander 2582ddd6f6 Error handling: Add info on the underlying system error to error alerts
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-17 19:04:17 +05:30
Jason A. Donenfeld 9556901a33 Version bump
This is our first release to the real app store.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-17 14:08:17 +01:00
Eric Kuck ed9b4c85ed Got TunnelsManager back under the max file length by splitting out NEVPNStatus+CustomStringConvertible
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-16 23:51:25 -06:00
Eric Kuck fc452753a7 Potential fix for insertRowAtIndexPath crash
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-16 21:11:33 -06:00
Jason A. Donenfeld d775682ef4 More proper way to get sdk root directory
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-17 00:23:11 +01:00
Jason A. Donenfeld ae339a000e Further generalize makefile
This should allow us to eventually build on macOS

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 03:51:43 +01:00
Jason A. Donenfeld a80cf6a0dc Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:55:04 +01:00
Jason A. Donenfeld 727992f5d2 Improve mock tunnels generation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:51:14 +01:00
Roopesh Chander 2a22c0f2d6 Provide mock tunnels for the Simulator
To help in generation of screenshots for the App Store

Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-16 01:15:10 +05:30
Jason A. Donenfeld b3f5635f4e Nuke duplicate file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:21:49 +01:00
Jason A. Donenfeld 946524aa8b Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:14:05 +01:00
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
76 changed files with 4585 additions and 4051 deletions
+4
View File
@@ -19,6 +19,7 @@ DerivedData
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xcscheme
## Other
*.xccheckout
@@ -38,3 +39,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:
```
+24
View File
@@ -0,0 +1,24 @@
disabled_rules:
- line_length
- trailing_whitespace
- todo
opt_in_rules:
- empty_count
- empty_string
- implicitly_unwrapped_optional
- legacy_random
- let_var_whitespace
- literal_expression_end_indentation
- override_in_extension
- redundant_type_annotation
- toggle_bool
- unneeded_parentheses_in_closure_argument
- unused_import
# - trailing_closure
file_length:
warning: 500
cyclomatic_complexity:
warning: 10
error: 25
function_body_length:
warning: 45
+21 -6
View File
@@ -5,23 +5,38 @@ import Foundation
import os.log
extension FileManager {
static var networkExtensionLogFileURL: URL? {
private static var sharedFolderURL: URL? {
guard let appGroupId = Bundle.main.object(forInfoDictionaryKey: "com.wireguard.ios.app_group_id") as? String else {
os_log("Can't obtain app group id from bundle", log: OSLog.default, type: .error)
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 {
os_log("Can't obtain shared folder URL", log: OSLog.default, type: .error)
wg_log(.error, message: "Cannot obtain shared folder URL")
return nil
}
return sharedFolderURL.appendingPathComponent("last-activated-tunnel-log.txt")
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(let e) {
os_log("Failed to delete file '%{public}@': %{public}@", log: OSLog.default, type: .debug, url.absoluteString, e.localizedDescription)
} catch {
return false
}
return true
+68
View File
@@ -0,0 +1,68 @@
// 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
@@ -1,57 +0,0 @@
// 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]
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 != 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") }
}
}
+13 -35
View File
@@ -4,15 +4,25 @@
import Foundation
import Network
@available(OSX 10.14, iOS 12.0, *)
struct DNSServer {
let address: IPAddress
init(address: IPAddress) {
self.address = address
}
}
// MARK: Converting to and from String
// For use in the UI
extension DNSServer: Equatable {
static func == (lhs: DNSServer, rhs: DNSServer) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue
}
}
extension DNSServer {
var stringRepresentation: String {
return "\(address)"
}
init?(from addressString: String) {
if let addr = IPv4Address(addressString) {
address = addr
@@ -22,36 +32,4 @@ extension DNSServer {
return nil
}
}
func stringRepresentation() -> String {
return "\(address)"
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension DNSServer: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(address.rawValue)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch (data.count) {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
}
+41 -48
View File
@@ -4,28 +4,53 @@
import Foundation
import Network
@available(OSX 10.14, iOS 12.0, *)
struct Endpoint {
let host: NWEndpoint.Host
let port: NWEndpoint.Port
init(host: NWEndpoint.Host, port: NWEndpoint.Port) {
self.host = host
self.port = port
}
}
// MARK: Converting to and from String
// For use in the UI
extension Endpoint: Equatable {
static func == (lhs: Endpoint, rhs: Endpoint) -> Bool {
return lhs.host == rhs.host && lhs.port == rhs.port
}
}
extension Endpoint: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(host)
hasher.combine(port)
}
}
extension Endpoint {
var stringRepresentation: String {
switch host {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
}
}
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,66 +60,34 @@ 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) {
case .name(let hostname, _):
return "\(hostname):\(port)"
case .ipv4(let address):
return "\(address):\(port)"
case .ipv6(let address):
return "[\(address)]:\(port)"
}
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension Endpoint: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.stringRepresentation())
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let endpointString = try container.decode(String.self)
guard let endpoint = Endpoint(from: endpointString) else {
throw DecodingError.invalidData
}
self = endpoint
}
enum DecodingError: Error {
case invalidData
}
}
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) {
switch host {
case .name(let hostname, _):
return hostname
case .ipv4(_):
case .ipv4:
return nil
case .ipv6(_):
case .ipv6:
return nil
}
}
+33 -52
View File
@@ -4,17 +4,41 @@
import Foundation
import Network
@available(OSX 10.14, iOS 12.0, *)
struct IPAddressRange {
let address: IPAddress
var networkPrefixLength: UInt8
init(address: IPAddress, networkPrefixLength: UInt8) {
self.address = address
self.networkPrefixLength = networkPrefixLength
}
}
// MARK: Converting to and from String
// For use in the UI
extension IPAddressRange: Equatable {
static func == (lhs: IPAddressRange, rhs: IPAddressRange) -> Bool {
return lhs.address.rawValue == rhs.address.rawValue && lhs.networkPrefixLength == rhs.networkPrefixLength
}
}
extension IPAddressRange: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(address.rawValue)
hasher.combine(networkPrefixLength)
}
}
extension IPAddressRange {
var stringRepresentation: String {
return "\(address)/\(networkPrefixLength)"
}
init?(from string: String) {
guard let parsed = IPAddressRange.parseAddressString(string) else { return nil }
address = parsed.0
networkPrefixLength = parsed.1
}
private static func parseAddressString(_ string: String) -> (IPAddress, UInt8)? {
let endOfIPAddress = string.lastIndex(of: "/") ?? string.endIndex
let addressString = String(string[string.startIndex ..< endOfIPAddress])
let address: IPAddress
@@ -25,62 +49,19 @@ extension IPAddressRange {
} else {
return nil
}
let maxNetworkPrefixLength: UInt8 = (address is IPv4Address) ? 32 : 128
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)
} else {
networkPrefixLength = maxNetworkPrefixLength
}
self.address = address
self.networkPrefixLength = networkPrefixLength
}
func stringRepresentation() -> String {
return "\(address)/\(networkPrefixLength)"
}
}
// MARK: Codable
// For serializing to disk
@available(OSX 10.14, iOS 12.0, *)
extension IPAddressRange: Codable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let addressDataLength: Int
if address is IPv4Address {
addressDataLength = 4
} else if address is IPv6Address {
addressDataLength = 16
} else {
fatalError()
}
var data = Data(capacity: addressDataLength + 1)
data.append(address.rawValue)
data.append(networkPrefixLength)
try container.encode(data)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch (data.count) {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
address = ipAddress
}
enum DecodingError: Error {
case invalidData
return (address, networkPrefixLength)
}
}
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
struct InterfaceConfiguration {
var privateKey: Data
var addresses = [IPAddressRange]()
var listenPort: UInt16?
var mtu: UInt16?
var dns = [DNSServer]()
init(privateKey: Data) {
if privateKey.count != TunnelConfiguration.keyLength {
fatalError("Invalid private key")
}
self.privateKey = privateKey
}
}
extension InterfaceConfiguration: Equatable {
static func == (lhs: InterfaceConfiguration, rhs: InterfaceConfiguration) -> Bool {
let lhsAddresses = lhs.addresses.filter { $0.address is IPv4Address } + lhs.addresses.filter { $0.address is IPv6Address }
let rhsAddresses = rhs.addresses.filter { $0.address is IPv4Address } + rhs.addresses.filter { $0.address is IPv6Address }
return lhs.privateKey == rhs.privateKey &&
lhsAddresses == rhsAddresses &&
lhs.listenPort == rhs.listenPort &&
lhs.mtu == rhs.mtu &&
lhs.dns == rhs.dns
}
}
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import Network
import NetworkExtension
protocol LegacyModel: Decodable {
associatedtype Model
var migrated: Model { get }
}
struct LegacyDNSServer: LegacyModel {
let address: IPAddress
var migrated: DNSServer {
return DNSServer(address: address)
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else {
throw DecodingError.invalidData
}
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
}
extension Array where Element == LegacyDNSServer {
var migrated: [DNSServer] {
return map { $0.migrated }
}
}
struct LegacyEndpoint: LegacyModel {
let host: Network.NWEndpoint.Host
let port: Network.NWEndpoint.Port
var migrated: Endpoint {
return Endpoint(host: host, port: port)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let endpointString = try container.decode(String.self)
guard !endpointString.isEmpty else { throw DecodingError.invalidData }
let startOfPort: String.Index
let hostString: String
if endpointString.first! == "[" {
// Look for IPv6-style endpoint, like [::1]:80
let startOfHost = endpointString.index(after: endpointString.startIndex)
guard let endOfHost = endpointString.dropFirst().firstIndex(of: "]") else { throw DecodingError.invalidData }
let afterEndOfHost = endpointString.index(after: endOfHost)
guard endpointString[afterEndOfHost] == ":" else { throw DecodingError.invalidData }
startOfPort = endpointString.index(after: afterEndOfHost)
hostString = String(endpointString[startOfHost ..< endOfHost])
} else {
// Look for an IPv4-style endpoint, like 127.0.0.1:80
guard let endOfHost = endpointString.firstIndex(of: ":") else { throw DecodingError.invalidData }
startOfPort = endpointString.index(after: endOfHost)
hostString = String(endpointString[endpointString.startIndex ..< endOfHost])
}
guard let endpointPort = NWEndpoint.Port(String(endpointString[startOfPort ..< endpointString.endIndex])) else { throw DecodingError.invalidData }
let invalidCharacterIndex = hostString.unicodeScalars.firstIndex { char in
return !CharacterSet.urlHostAllowed.contains(char)
}
guard invalidCharacterIndex == nil else { throw DecodingError.invalidData }
host = NWEndpoint.Host(hostString)
port = endpointPort
}
enum DecodingError: Error {
case invalidData
}
}
struct LegacyInterfaceConfiguration: LegacyModel {
let name: String
let privateKey: Data
let addresses: [LegacyIPAddressRange]
let listenPort: UInt16?
let mtu: UInt16?
let dns: [LegacyDNSServer]
var migrated: InterfaceConfiguration {
var interface = InterfaceConfiguration(privateKey: privateKey)
interface.addresses = addresses.migrated
interface.listenPort = listenPort
interface.mtu = mtu
interface.dns = dns.migrated
return interface
}
}
struct LegacyIPAddressRange: LegacyModel {
let address: IPAddress
let networkPrefixLength: UInt8
var migrated: IPAddressRange {
return IPAddressRange(address: address, networkPrefixLength: networkPrefixLength)
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var data = try container.decode(Data.self)
networkPrefixLength = data.removeLast()
let ipAddressFromData: IPAddress? = {
switch data.count {
case 4: return IPv4Address(data)
case 16: return IPv6Address(data)
default: return nil
}
}()
guard let ipAddress = ipAddressFromData else { throw DecodingError.invalidData }
address = ipAddress
}
enum DecodingError: Error {
case invalidData
}
}
extension Array where Element == LegacyIPAddressRange {
var migrated: [IPAddressRange] {
return map { $0.migrated }
}
}
struct LegacyPeerConfiguration: LegacyModel {
let publicKey: Data
let preSharedKey: Data?
let allowedIPs: [LegacyIPAddressRange]
let endpoint: LegacyEndpoint?
let persistentKeepAlive: UInt16?
var migrated: PeerConfiguration {
var configuration = PeerConfiguration(publicKey: publicKey)
configuration.preSharedKey = preSharedKey
configuration.allowedIPs = allowedIPs.migrated
configuration.endpoint = endpoint?.migrated
configuration.persistentKeepAlive = persistentKeepAlive
return configuration
}
}
extension Array where Element == LegacyPeerConfiguration {
var migrated: [PeerConfiguration] {
return map { $0.migrated }
}
}
final class LegacyTunnelConfiguration: LegacyModel {
let interface: LegacyInterfaceConfiguration
let peers: [LegacyPeerConfiguration]
var migrated: TunnelConfiguration {
return TunnelConfiguration(name: interface.name, interface: interface.migrated, peers: peers.migrated)
}
}
extension NETunnelProviderProtocol {
@discardableResult
func migrateConfigurationIfNeeded() -> Bool {
guard let configurationVersion = providerConfiguration?["tunnelConfigurationVersion"] as? Int else { return false }
if configurationVersion == 1 {
migrateFromConfigurationV1()
} else {
fatalError("No migration from configuration version \(configurationVersion) exists.")
}
return true
}
private func migrateFromConfigurationV1() {
guard let serializedTunnelConfiguration = providerConfiguration?["tunnelConfiguration"] as? Data else { return }
guard let configuration = try? JSONDecoder().decode(LegacyTunnelConfiguration.self, from: serializedTunnelConfiguration) else { return }
providerConfiguration = [Keys.wgQuickConfig.rawValue: configuration.migrated.asWgQuickConfig()]
}
}
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum PacketTunnelProviderError: String, Error {
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure
case couldNotStartBackend
case couldNotDetermineFileDescriptor
case couldNotSetNetworkSettings
}
extension NETunnelProviderProtocol {
enum Keys: String {
case wgQuickConfig = "WgQuickConfig"
}
convenience init?(tunnelConfiguration: TunnelConfiguration) {
self.init()
let appId = Bundle.main.bundleIdentifier!
providerBundleIdentifier = "\(appId).network-extension"
providerConfiguration = [Keys.wgQuickConfig.rawValue: tunnelConfiguration.asWgQuickConfig()]
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
if endpoints.count == 1 {
serverAddress = endpoints[0].stringRepresentation
} else if endpoints.isEmpty {
serverAddress = "Unspecified"
} else {
serverAddress = "Multiple endpoints"
}
}
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
migrateConfigurationIfNeeded()
guard let serializedConfig = providerConfiguration?[Keys.wgQuickConfig.rawValue] as? String else { return nil }
return try? TunnelConfiguration(fromWgQuickConfig: serializedConfig, called: name)
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
struct PeerConfiguration {
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")
}
}
}
extension PeerConfiguration: Equatable {
static func == (lhs: PeerConfiguration, rhs: PeerConfiguration) -> Bool {
return lhs.publicKey == rhs.publicKey &&
lhs.preSharedKey == rhs.preSharedKey &&
Set(lhs.allowedIPs) == Set(rhs.allowedIPs) &&
lhs.endpoint == rhs.endpoint &&
lhs.persistentKeepAlive == rhs.persistentKeepAlive
}
}
extension PeerConfiguration: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(publicKey)
hasher.combine(preSharedKey)
hasher.combine(Set(allowedIPs))
hasher.combine(endpoint)
hasher.combine(persistentKeepAlive)
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
extension String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
return split(separator: separator)
.map {
if let charSet = trimmingCharacters {
return $0.trimmingCharacters(in: charSet)
} else {
return String($0)
}
}
}
}
extension Optional where Wrapped == String {
func splitToArray(separator: Character = ",", trimmingCharacters: CharacterSet? = nil) -> [String] {
switch self {
case .none:
return []
case .some(let wrapped):
return wrapped.splitToArray(separator: separator, trimmingCharacters: trimmingCharacters)
}
}
}
@@ -0,0 +1,200 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
extension TunnelConfiguration {
enum ParserState {
case inInterfaceSection
case inPeerSection
case notInASection
}
enum ParseError: Error {
case invalidLine(_ line: String.SubSequence)
case noInterface
case invalidInterface
case multipleInterfaces
case multiplePeersWithSamePublicKey
case invalidPeer
}
//swiftlint:disable:next function_body_length cyclomatic_complexity
convenience init(fromWgQuickConfig wgQuickConfig: String, called name: String? = nil) throws {
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]()
let lines = wgQuickConfig.split(separator: "\n")
var parserState = ParserState.notInASection
var attributes = [String: String]()
for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String
if let commentRange = line.range(of: "#") {
trimmedLine = String(line[..<commentRange.lowerBound])
} else {
trimmedLine = String(line)
}
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
guard !trimmedLine.isEmpty else { continue }
let lowercasedLine = line.lowercased()
if let equalsIndex = line.firstIndex(of: "=") {
// Line contains an attribute
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"]
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)
}
let isLastLine = lineIndex == lines.count - 1
if isLastLine || lowercasedLine == "[interface]" || lowercasedLine == "[peer]" {
// Previous section has ended; process the attributes collected so far
if parserState == .inInterfaceSection {
guard let interface = TunnelConfiguration.collate(interfaceAttributes: attributes) else { throw ParseError.invalidInterface }
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
} else if parserState == .inPeerSection {
guard let peer = TunnelConfiguration.collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer)
}
}
if lowercasedLine == "[interface]" {
parserState = .inInterfaceSection
attributes.removeAll()
} else if lowercasedLine == "[peer]" {
parserState = .inPeerSection
attributes.removeAll()
}
}
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey
}
if let interfaceConfiguration = interfaceConfiguration {
self.init(name: name, interface: interfaceConfiguration, peers: peerConfigurations)
} else {
throw ParseError.noInterface
}
}
func asWgQuickConfig() -> String {
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 {
let addressString = interface.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if !interface.dns.isEmpty {
let dnsString = interface.dns.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
if let mtu = interface.mtu {
output.append("MTU = \(mtu)\n")
}
for peer in 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 {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
if let endpoint = peer.endpoint {
output.append("Endpoint = \(endpoint.stringRepresentation)\n")
}
if let persistentKeepAlive = peer.persistentKeepAlive {
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
}
}
return output
}
//swiftlint:disable:next cyclomatic_complexity
private static 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 == TunnelConfiguration.keyLength else { return nil }
var interface = InterfaceConfiguration(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.splitToArray(trimmingCharacters: .whitespaces) {
guard let address = IPAddressRange(from: addressString) else { return nil }
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespaces) {
guard let dnsServer = DNSServer(from: dnsServerString) 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.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let allowedIP = IPAddressRange(from: allowedIPString) 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
}
}
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
final class TunnelConfiguration {
var name: String?
var interface: InterfaceConfiguration
let peers: [PeerConfiguration]
static let keyLength = 32
init(name: String?, interface: InterfaceConfiguration, peers: [PeerConfiguration]) {
self.interface = interface
self.peers = peers
self.name = name
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")
}
}
}
extension TunnelConfiguration: Equatable {
static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool {
return lhs.name == rhs.name &&
lhs.interface == rhs.interface &&
Set(lhs.peers) == Set(rhs.peers)
}
}
@@ -1,35 +0,0 @@
// 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)
}
}
+201 -46
View File
@@ -7,8 +7,25 @@
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 */; };
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 */; };
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */; };
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */; };
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */; };
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */; };
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */; };
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.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 */; };
@@ -20,20 +37,21 @@
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */; };
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; };
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */; };
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E9217229DB006A79B3 /* IPAddressRange.swift */; };
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
6F7774F321774263006A79B3 /* TunnelEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */; };
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; };
6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; };
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; };
6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; };
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; };
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; };
@@ -41,14 +59,18 @@
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */; };
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 */; };
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6FE1765421C90BBE002690EA /* Localizable.strings */; };
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE1765921C90E87002690EA /* LocalizationHelper.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 */; };
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF717E421B2CB1E0045A474 /* InternetReachability.swift */; };
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */; };
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* Configuration.swift */; };
6FFA5D8E2194370D0001E2F7 /* TunnelConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E72172020C006A79B3 /* TunnelConfiguration.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 */; };
@@ -91,8 +113,20 @@
/* 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>"; };
5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ArrayConversion.swift"; sourceTree = "<group>"; };
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyConfigMigration.swift; sourceTree = "<group>"; };
5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+WgQuickConfig.swift"; sourceTree = "<group>"; };
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceConfiguration.swift; sourceTree = "<group>"; };
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerConfiguration.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; };
@@ -110,20 +144,21 @@
6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; };
6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; };
6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; };
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileParser.swift; sourceTree = "<group>"; };
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
6F7774E72172020C006A79B3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelConfiguration.swift; sourceTree = "<group>"; };
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPAddressRange.swift; sourceTree = "<group>"; };
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsManager.swift; sourceTree = "<group>"; };
6F7774F221774263006A79B3 /* TunnelEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditTableViewController.swift; sourceTree = "<group>"; };
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = "<group>"; };
6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; };
6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; };
6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; };
6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; };
@@ -134,9 +169,13 @@
6FDEF7FF21863C0100D8FBF6 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = "<group>"; };
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>"; };
6FE1765521C90BBE002690EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/Localizable.strings; sourceTree = "<group>"; };
6FE1765921C90E87002690EA /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.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>"; };
@@ -144,7 +183,6 @@
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>"; };
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternetReachability.swift; 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>"; };
@@ -169,6 +207,33 @@
/* 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 = (
@@ -186,8 +251,8 @@
6F5D0C432183B4A4000F85AD /* Shared */ = {
isa = PBXGroup;
children = (
6FF3526A21C23F720008484E /* Logging */,
6F7774E6217201E0006A79B3 /* Model */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */,
);
path = Shared;
@@ -203,15 +268,6 @@
path = Crypto;
sourceTree = "<group>";
};
6F6899AA218099D00012E523 /* ConfigFile */ = {
isa = PBXGroup;
children = (
6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */,
6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */,
);
path = ConfigFile;
sourceTree = "<group>";
};
6F7774DD217181B1006A79B3 /* UI */ = {
isa = PBXGroup;
children = (
@@ -224,16 +280,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>";
@@ -241,22 +292,30 @@
6F7774E6217201E0006A79B3 /* Model */ = {
isa = PBXGroup;
children = (
6F7774E72172020C006A79B3 /* Configuration.swift */,
5F9696AF21CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift */,
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */,
5F4541B121CBFAEE00994C13 /* String+ArrayConversion.swift */,
5F9696A921CD6AE6008063FE /* LegacyConfigMigration.swift */,
6F7774E72172020C006A79B3 /* TunnelConfiguration.swift */,
6F7774E9217229DB006A79B3 /* IPAddressRange.swift */,
6F693A552179E556008551C1 /* Endpoint.swift */,
6F628C3E217F3413003482A3 /* DNSServer.swift */,
5FF7B96121CC95DE00A7DD74 /* InterfaceConfiguration.swift */,
5FF7B96421CC95FA00A7DD74 /* PeerConfiguration.swift */,
);
path = Model;
sourceTree = "<group>";
};
6F7774ED21722D0C006A79B3 /* VPN */ = {
6F7774ED21722D0C006A79B3 /* Tunnel */ = {
isa = PBXGroup;
children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */,
6FF717E421B2CB1E0045A474 /* InternetReachability.swift */,
5F4541A821C451D100994C13 /* TunnelStatus.swift */,
6FB1017821C57DE600766195 /* MockTunnels.swift */,
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */,
);
path = VPN;
path = Tunnel;
sourceTree = "<group>";
};
6F919ED3218C65C50023B400 /* Resources */ = {
@@ -310,9 +369,20 @@
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 = (
6FE1765421C90BBE002690EA /* Localizable.strings */,
6F5D0C432183B4A4000F85AD /* Shared */,
6FF4AC16211EC46F002C96EB /* WireGuard */,
6F5D0C1B218352EF000F85AD /* WireGuardNetworkExtension */,
@@ -335,12 +405,12 @@
children = (
6F919ED3218C65C50023B400 /* Resources */,
6F6899A32180445A0012E523 /* Crypto */,
6F6899AA218099D00012E523 /* ConfigFile */,
6F7774DD217181B1006A79B3 /* UI */,
6F7774ED21722D0C006A79B3 /* VPN */,
6F7774ED21722D0C006A79B3 /* Tunnel */,
6FDEF7E72186320E00D8FBF6 /* ZipArchive */,
6F61F1E821B932F700483816 /* WireGuardAppError.swift */,
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */,
6FE1765921C90E87002690EA /* LocalizationHelper.swift */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
@@ -384,6 +454,7 @@
isa = PBXNativeTarget;
buildConfigurationList = 6F5D0C25218352EF000F85AD /* Build configuration list for PBXNativeTarget "WireGuardNetworkExtension" */;
buildPhases = (
5F45417B21C0906F00994C13 /* Swiftlint */,
6F61F1EC21BA4D4700483816 /* Extract wireguard-go Version */,
6F5D0C16218352EF000F85AD /* Sources */,
6F5D0C17218352EF000F85AD /* Frameworks */,
@@ -403,6 +474,8 @@
isa = PBXNativeTarget;
buildConfigurationList = 6FF4AC26211EC472002C96EB /* Build configuration list for PBXNativeTarget "WireGuard" */;
buildPhases = (
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */,
5F45417A21C0902400994C13 /* Swiftlint */,
6B87860E2189532500C099FB /* Extract wireguard-go Version */,
6FF4AC10211EC46F002C96EB /* Sources */,
6FF4AC11211EC46F002C96EB /* Frameworks */,
@@ -483,6 +556,7 @@
buildActionMask = 2147483647;
files = (
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */,
@@ -494,6 +568,60 @@
/* 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";
};
5F784E5721CDF6DD00B8D9A0 /* Strip Trailing Whitespace */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Strip Trailing Whitespace";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "find . -name '*.swift' -exec sed -i '' -E 's/[[:space:]]+$//g' {} +\n";
};
6B87860E2189532500C099FB /* Extract wireguard-go Version */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -539,14 +667,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6FF3527021C240160008484E /* ringlogger.c in Sources */,
6FF3527121C240160008484E /* Logger.swift in Sources */,
6F5A2B4621AFDED40081EDD8 /* FileManager+Extension.swift in Sources */,
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */,
5F9696B121CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
6FFA5D8E2194370D0001E2F7 /* Configuration.swift in Sources */,
6FFA5D8E2194370D0001E2F7 /* TunnelConfiguration.swift in Sources */,
5FF7B96621CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
5F9696AE21CD6F72008063FE /* String+ArrayConversion.swift in Sources */,
6FFA5D8F2194370D0001E2F7 /* IPAddressRange.swift in Sources */,
6FFA5D902194370D0001E2F7 /* Endpoint.swift in Sources */,
5FF7B96321CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
6FFA5D9321943BC90001E2F7 /* DNSResolver.swift in Sources */,
6FFA5D912194370D0001E2F7 /* DNSServer.swift in Sources */,
5F9696AB21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
6FFA5D8921942F320001E2F7 /* PacketTunnelSettingsGenerator.swift in Sources */,
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */,
);
@@ -556,38 +691,52 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6F6899AC218099F00012E523 /* WgQuickConfigFileParser.swift in Sources */,
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */,
6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.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 */,
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */,
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift 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 */,
5F9696AA21CD6AE6008063FE /* LegacyConfigMigration.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */,
5F45418C21C2D48200994C13 /* TunnelEditKeyValueCell.swift in Sources */,
6FE254FB219C10800028284D /* ZipImporter.swift in Sources */,
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* Configuration.swift in Sources */,
6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.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 */,
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */,
6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */,
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */,
6FF717E521B2CB1E0045A474 /* InternetReachability.swift in Sources */,
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -607,6 +756,14 @@
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
6FE1765421C90BBE002690EA /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
6FE1765521C90BBE002690EA /* Base */,
);
name = Localizable.strings;
sourceTree = "<group>";
};
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
@@ -820,7 +977,6 @@
"$(inherited)",
"@executable_path/Frameworks",
);
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
@@ -840,7 +996,6 @@
"$(inherited)",
"@executable_path/Frameworks",
);
"OTHER_CFLAGS[arch=*]" = "-DNOCRYPT";
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "WireGuard/WireGuard-Bridging-Header.h";
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
<device id="ipad9_7" orientation="landscape">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -23,7 +23,7 @@
<placeholder placeholderIdentifier="IBFirstResponder" id="XJD-RE-ATd" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
<!--WireGuard-->
<!--Table View Controller-->
<scene sceneID="pXL-Um-fWR">
<objects>
<tableViewController clearsSelectionOnViewWillAppear="NO" id="9s0-Gz-xEQ" sceneMemberID="viewController">
@@ -46,10 +46,7 @@
<outlet property="delegate" destination="9s0-Gz-xEQ" id="Frz-TZ-bD1"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="WireGuard" id="0ZE-eq-OPY">
<barButtonItem key="leftBarButtonItem" title="Settings" id="a9Y-E3-hac"/>
<barButtonItem key="rightBarButtonItem" systemItem="add" id="ZSm-4E-cJx"/>
</navigationItem>
<navigationItem key="navigationItem" id="0ZE-eq-OPY"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="YJ1-t3-sLe" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
@@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Generic alert action names
"actionOK" = "OK";
"actionCancel" = "Cancel";
"actionSave" = "Save";
// Tunnels list UI
"tunnelsListTitle" = "WireGuard";
"tunnelsListSettingsButtonTitle" = "Settings";
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
"tunnelsListSwipeDeleteButtonTitle" = "Delete";
// Tunnels list menu
"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
"addTunnelMenuImportFile" = "Create from file or archive";
"addTunnelMenuQRCode" = "Create from QR code";
"addTunnelMenuFromScratch" = "Create from scratch";
// Tunnels list alerts
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
"alertUnableToImportTitle" = "Unable to import tunnel";
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration.";
// Tunnel detail and edit UI
"newTunnelViewTitle" = "New configuration";
"editTunnelViewTitle" = "Edit configuration";
"tunnelSectionTitleStatus" = "Status";
"tunnelStatusInactive" = "Inactive";
"tunnelStatusActivating" = "Activating";
"tunnelStatusActive" = "Active";
"tunnelStatusDeactivating" = "Deactivating";
"tunnelStatusReasserting" = "Reactivating";
"tunnelStatusRestarting" = "Restarting";
"tunnelStatusWaiting" = "Waiting";
"tunnelSectionTitleInterface" = "Interface";
"tunnelInterfaceName" = "Name";
"tunnelInterfacePrivateKey" = "Private key";
"tunnelInterfacePublicKey" = "Public key";
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
"tunnelInterfaceAddresses" = "Addresses";
"tunnelInterfaceListenPort" = "Listen port";
"tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS servers";
"tunnelSectionTitlePeer" = "Peer";
"tunnelPeerPublicKey" = "Public key";
"tunnelPeerPreSharedKey" = "Preshared key";
"tunnelPeerEndpoint" = "Endpoint";
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
"tunnelPeerAllowedIPs" = "Allowed IPs";
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
"tunnelOnDemandKey" = "Activate on demand";
"tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
"addPeerButtonTitle" = "Add peer";
"deletePeerButtonTitle" = "Delete peer";
"deletePeerConfirmationAlertButtonTitle" = "Delete";
"deletePeerConfirmationAlertMessage" = "Delete this peer?";
"deleteTunnelButtonTitle" = "Delete tunnel";
"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
"tunnelEditPlaceholderTextRequired" = "Required";
"tunnelEditPlaceholderTextOptional" = "Optional";
"tunnelEditPlaceholderTextAutomatic" = "Automatic";
"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
"tunnelEditPlaceholderTextOff" = "Off";
// Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */
"alertInvalidInterfaceTitle" = "Invalid interface";
/* Any one of the following alert messages can go with the above title */
"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface's private key is required";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface's private key must be a 32-byte key in base64 encoding";
"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidInterfaceMessageListenPortInvalid" = "Interface's listen port must be between 0 and 65535, or unspecified";
"alertInvalidInterfaceMessageMTUInvalid" = "Interface's MTU must be between 576 and 65535, or unspecified";
"alertInvalidInterfaceMessageDNSInvalid" = "Interface's DNS servers must be a list of comma-separated IP addresses";
/* Alert title for error in the peer data */
"alertInvalidPeerTitle" = "Invalid peer";
/* Any one of the following alert messages can go with the above title */
"alertInvalidPeerMessagePublicKeyRequired" = "Peer's public key is required";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peer's public key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer's preshared key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidPeerMessageEndpointInvalid" = "Peer's endpoint must be of the form 'host:port' or '[host]:port'";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer's persistent keepalive must be between 0 to 65535, or unspecified";
"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
// Scanning QR code UI
"scanQRCodeViewTitle" = "Scan QR code";
"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
// Scanning QR code alerts
"alertScanQRCodeCameraUnsupportedTitle" = "Camera Unsupported";
"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
"alertScanQRCodeInvalidQRCodeTitle" = "Invalid QR Code";
"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
"alertScanQRCodeUnreadableQRCodeTitle" = "Invalid Code";
"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
// Settings UI
"settingsViewTitle" = "Settings";
"settingsSectionTitleAbout" = "About";
"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
"settingsSectionTitleExportConfigurations" = "Export configurations";
"settingsExportZipButtonTitle" = "Export zip archive";
"settingsSectionTitleTunnelLog" = "Tunnel log";
"settingsExportLogFileButtonTitle" = "Export log file";
// Settings alerts
"alertUnableToRemovePreviousLogTitle" = "Log export failed";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
"alertUnableToWriteLogTitle" = "Log export failed";
"alertUnableToWriteLogMessage" = "Unable to write logs to file";
// Zip import / export error alerts
"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
"alertBadArchiveTitle" = "Unable to read zip archive";
"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
"alertNoTunnelsToExportTitle" = "Nothing to export";
"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
// Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Activation failure";
"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
"alertTunnelDNSFailureTitle" = "DNS resolution failure";
"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
"alertTunnelNameEmptyTitle" = "No name provided";
"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
// Tunnel management error alerts on system error
/* The alert message that goes with the following titles would be
one of the alertSystemErrorMessage* listed further down */
"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
/* The alert message for this alert shall include
one of the alertSystemErrorMessage* listed further down */
"alertTunnelActivationSystemErrorTitle" = "Activation failure";
"alertTunnelActivationSystemErrorMessage (%@)" = "The tunnel could not be activated. %@";
/* alertSystemErrorMessage* messages */
"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104
VERSION_ID = 5
VERSION_NAME = 0.0.20181225
VERSION_ID = 2
@@ -1,166 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
class WgQuickConfigFileParser {
enum ParserState {
case inInterfaceSection
case inPeerSection
case notInASection
}
enum ParseError: Error {
case invalidLine(_ line: String.SubSequence)
case noInterface
case invalidInterface
case multipleInterfaces
case multiplePeersWithSamePublicKey
case invalidPeer
}
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] = []
let lines = text.split(separator: "\n")
var parserState: ParserState = .notInASection
var attributes: [String: String] = [:]
for (lineIndex, line) in lines.enumerated() {
var trimmedLine: String
if let commentRange = line.range(of: "#") {
trimmedLine = String(line[..<commentRange.lowerBound])
} else {
trimmedLine = String(line)
}
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
guard trimmedLine.count > 0 else { continue }
let lowercasedLine = line.lowercased()
if let equalsIndex = line.firstIndex(of: "=") {
// Line contains an attribute
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"]
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)
}
}
let isLastLine: Bool = (lineIndex == lines.count - 1)
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 }
interfaceConfiguration = interface
} else if (parserState == .inPeerSection) {
guard let peer = collate(peerAttributes: attributes) else { throw ParseError.invalidPeer }
peerConfigurations.append(peer)
}
}
if (lowercasedLine == "[interface]") {
parserState = .inInterfaceSection
attributes.removeAll()
} else if (lowercasedLine == "[peer]") {
parserState = .inPeerSection
attributes.removeAll()
}
}
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
throw ParseError.multiplePeersWithSamePublicKey
}
if let interfaceConfiguration = interfaceConfiguration {
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
return tunnelConfiguration
} else {
throw ParseError.noInterface
}
}
}
@@ -1,46 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
class WgQuickConfigFileWriter {
static func writeConfigFile(from tc: TunnelConfiguration) -> Data? {
let interface = tc.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) {
let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("Address = \(addressString)\n")
}
if (!interface.dns.isEmpty) {
let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("DNS = \(dnsString)\n")
}
if let mtu = interface.mtu {
output.append("MTU = \(mtu)\n")
}
for peer in tc.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) {
let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
output.append("AllowedIPs = \(allowedIPsString)\n")
}
if let endpoint = peer.endpoint {
output.append("Endpoint = \(endpoint.stringRepresentation())\n")
}
if let persistentKeepAlive = peer.persistentKeepAlive {
output.append("PersistentKeepalive = \(persistentKeepAlive)\n")
}
}
return output.data(using: .utf8)
}
}
+11 -8
View File
@@ -4,24 +4,27 @@
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
}
}
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
func tr(_ key: String) -> String {
return NSLocalizedString(key, comment: "")
}
func tr(format: String, _ arguments: CVarArg...) -> String {
return String(format: NSLocalizedString(format, comment: ""), arguments: arguments)
}
@@ -21,7 +21,7 @@ extension ActivateOnDemandSetting {
let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect()
switch (activateOnDemandOption) {
switch activateOnDemandOption {
case .none:
rules = nil
case .useOnDemandOverWiFiOrCellular:
@@ -41,7 +41,7 @@ extension ActivateOnDemandSetting {
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption
switch (rules.count) {
switch rules.count {
case 0:
activateOnDemandOption = .none
case 1:
@@ -51,9 +51,9 @@ extension ActivateOnDemandSetting {
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) {
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == .cellular {
activateOnDemandOption = .useOnDemandOverWiFiOnly
} else if (connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi) {
} else if connectRule.interfaceTypeMatch == .cellular && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .useOnDemandOverCellularOnly
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
@@ -62,10 +62,10 @@ extension ActivateOnDemandSetting {
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self.activateOnDemandOption = activateOnDemandOption
if (activateOnDemandOption == .none) {
self.isActivateOnDemandEnabled = false
if activateOnDemandOption == .none {
isActivateOnDemandEnabled = false
} else {
self.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
}
}
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
// Creates mock tunnels for the iOS Simulator.
#if targetEnvironment(simulator)
class MockTunnels {
static let tunnelNames = [
"demo",
"edgesecurity",
"home",
"office",
"infra-fr",
"infra-us",
"krantz",
"metheny",
"frisell"
]
static let address = "192.168.%d.%d/32"
static let dnsServers = ["8.8.8.8", "8.8.4.4"]
static let endpoint = "demo.wireguard.com:51820"
static let allowedIPs = "0.0.0.0/0"
static func createMockTunnels() -> [NETunnelProviderManager] {
return tunnelNames.map { tunnelName -> NETunnelProviderManager in
var interface = InterfaceConfiguration(privateKey: Curve25519.generatePrivateKey())
interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
interface.dns = dnsServers.map { DNSServer(from: $0)! }
var peer = PeerConfiguration(publicKey: Curve25519.generatePublicKey(fromPrivateKey: Curve25519.generatePrivateKey()))
peer.endpoint = Endpoint(from: endpoint)
peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
let tunnelConfiguration = TunnelConfiguration(name: tunnelName, interface: interface, peers: [peer])
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
tunnelProviderManager.isEnabled = true
return tunnelProviderManager
}
}
}
#endif
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum TunnelsManagerError: WireGuardAppError {
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case systemErrorOnListingTunnels(systemError: Error)
case systemErrorOnAddTunnel(systemError: Error)
case systemErrorOnModifyTunnel(systemError: Error)
case systemErrorOnRemoveTunnel(systemError: Error)
var alertText: AlertText {
switch self {
case .tunnelNameEmpty:
return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
case .tunnelAlreadyExistsWithThatName:
return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
case .systemErrorOnListingTunnels(let systemError):
return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
case .systemErrorOnAddTunnel(let systemError):
return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnModifyTunnel(let systemError):
return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
case .systemErrorOnRemoveTunnel(let systemError):
return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
}
}
}
enum TunnelsManagerActivationAttemptError: WireGuardAppError {
case tunnelIsNotInactive
case failedWhileStarting(systemError: Error) // startTunnel() throwed
case failedWhileSaving(systemError: Error) // save config after re-enabling throwed
case failedWhileLoading(systemError: Error) // reloading config throwed
case failedBecauseOfTooManyErrors(lastSystemError: Error) // recursion limit reached
var alertText: AlertText {
switch self {
case .tunnelIsNotInactive:
return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
case .failedWhileStarting(let systemError),
.failedWhileSaving(let systemError),
.failedWhileLoading(let systemError),
.failedBecauseOfTooManyErrors(let systemError):
return (tr("alertTunnelActivationSystemErrorTitle"),
tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
}
}
}
enum TunnelsManagerActivationError: WireGuardAppError {
case activationFailed(wasOnDemandEnabled: Bool)
case activationFailedWithExtensionError(title: String, message: String, wasOnDemandEnabled: Bool)
var alertText: AlertText {
switch self {
case .activationFailed(let wasOnDemandEnabled):
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
}
}
}
extension PacketTunnelProviderError: WireGuardAppError {
var alertText: AlertText {
switch self {
case .savedProtocolConfigurationIsInvalid:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSavedConfigFailureMessage"))
case .dnsResolutionFailure:
return (tr("alertTunnelDNSFailureTitle"), tr("alertTunnelDNSFailureMessage"))
case .couldNotStartBackend:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationBackendFailureMessage"))
case .couldNotDetermineFileDescriptor:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFileDescriptorFailureMessage"))
case .couldNotSetNetworkSettings:
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSetNetworkSettingsMessage"))
}
}
}
extension Error {
var localizedUIString: String {
if let systemError = self as? NEVPNError {
switch systemError {
case NEVPNError.configurationInvalid:
return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
case NEVPNError.configurationDisabled:
return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
case NEVPNError.connectionFailed:
return tr("alertSystemErrorMessageTunnelConnectionFailed")
case NEVPNError.configurationStale:
return tr("alertSystemErrorMessageTunnelConfigurationStale")
case NEVPNError.configurationReadWriteFailed:
return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
case NEVPNError.configurationUnknown:
return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
default:
return ""
}
} else {
return localizedDescription
}
}
}
@@ -0,0 +1,59 @@
// 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"
}
}
}
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"
}
}
}
@@ -0,0 +1,460 @@
// 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
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
private var statusObservationToken: AnyObject?
private var waiteeObservationToken: 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: MockTunnels.createMockTunnels())))
#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(systemError: error)))
return
}
let tunnelManagers = managers ?? []
tunnelManagers.forEach { tunnelManager in
if (tunnelManager.protocolConfiguration as? NETunnelProviderProtocol)?.migrateConfigurationIfNeeded() == true {
tunnelManager.saveToPreferences { _ in }
}
}
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
}
#endif
}
func reload(completionHandler: @escaping (Bool) -> Void) {
#if targetEnvironment(simulator)
completionHandler(false)
#else
NETunnelProviderManager.loadAllFromPreferences { managers, _ in
guard let managers = managers else {
completionHandler(false)
return
}
let newTunnels = managers.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name }
let hasChanges = self.tunnels.map { $0.tunnelConfiguration } != newTunnels.map { $0.tunnelConfiguration }
if hasChanges {
self.tunnels = newTunnels
completionHandler(true)
} else {
completionHandler(false)
}
}
#endif
}
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) {
let tunnelName = tunnelConfiguration.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 = tunnelConfiguration.name
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(systemError: error!)))
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.name ?? ""
if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty)
return
}
let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription
if isNameChanged {
guard !tunnels.contains(where: { $0.name == tunnelName }) else {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return
}
tunnel.name = tunnelName
}
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelConfiguration.name
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(systemError: error!))
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(systemError: error!))
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(systemError: error!))
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
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
if tunnelInOperation.status != .deactivating {
startDeactivation(of: tunnelInOperation)
}
return
}
#if targetEnvironment(simulator)
tunnel.status = .active
#else
tunnel.startActivation(activationDelegate: activationDelegate)
#endif
}
func startDeactivation(of tunnel: TunnelContainer) {
tunnel.isAttemptingActivation = false
guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
#if targetEnvironment(simulator)
tunnel.status = .inactive
#else
tunnel.startDeactivation()
#endif
}
func refreshStatuses() {
tunnels.forEach { $0.refreshStatus() }
}
private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
guard let self = self else { return }
if tunnel.status == .inactive {
if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
}
self.waiteeObservationToken = nil
}
}
}
private func startObservingTunnelStatuses() {
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 tunnelConfiguration = TunnelContainer(tunnel: tunnelProvider).tunnelConfiguration,
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
if tunnel.tunnelProvider != tunnelProvider {
tunnel.tunnelProvider = tunnelProvider
tunnel.refreshStatus()
}
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
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) = lastErrorTextFromNetworkExtension(for: tunnel) {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
} else {
self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
}
}
}
if (tunnel.status == .restarting) && (session.status == .disconnected || session.status == .disconnecting) {
if session.status == .disconnected {
tunnel.startActivation(activationDelegate: self.activationDelegate)
}
return
}
tunnel.refreshStatus()
}
}
}
private 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 lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
return extensionError.alertText
}
return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool
var isAttemptingActivation = false {
didSet {
if isAttemptingActivation {
self.activationTimer?.invalidate()
let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
guard let self = self else { return }
wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
switch self.tunnelProvider.connection.status {
case .connected, .disconnected, .invalid:
self.activationTimer?.invalidate()
self.activationTimer = nil
default:
break
}
self.refreshStatus()
}
self.activationTimer = activationTimer
RunLoop.main.add(activationTimer, forMode: .default)
}
}
}
var activationAttemptId: String?
var activationTimer: Timer?
fileprivate var tunnelProvider: NETunnelProviderManager
private var lastTunnelConnectionStatus: NEVPNStatus?
var tunnelConfiguration: TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: tunnelProvider.localizedDescription)
}
var activateOnDemandSetting: ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
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 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(lastSystemError: lastError!))
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(systemError: error!))
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, 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(systemError: error))
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(systemError: systemError))
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(systemError: systemError))
return
}
wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
}
}
}
fileprivate func startDeactivation() {
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
}
+157 -159
View File
@@ -3,31 +3,57 @@
import UIKit
//swiftlint:disable:next type_body_length
class TunnelViewModel {
enum InterfaceField: String {
case name = "Name"
case privateKey = "Private key"
case publicKey = "Public key"
case generateKeyPair = "Generate keypair"
case addresses = "Addresses"
case listenPort = "Listen port"
case mtu = "MTU"
case dns = "DNS servers"
enum InterfaceField {
case name
case privateKey
case publicKey
case generateKeyPair
case addresses
case listenPort
case mtu
case dns
var localizedUIString: String {
switch self {
case .name: return tr("tunnelInterfaceName")
case .privateKey: return tr("tunnelInterfacePrivateKey")
case .publicKey: return tr("tunnelInterfacePublicKey")
case .generateKeyPair: return tr("tunnelInterfaceGenerateKeypair")
case .addresses: return tr("tunnelInterfaceAddresses")
case .listenPort: return tr("tunnelInterfaceListenPort")
case .mtu: return tr("tunnelInterfaceMTU")
case .dns: return tr("tunnelInterfaceDNS")
}
}
}
static let interfaceFieldsWithControl: Set<InterfaceField> = [
.generateKeyPair
]
enum PeerField: String {
case publicKey = "Public key"
case preSharedKey = "Preshared key"
case endpoint = "Endpoint"
case persistentKeepAlive = "Persistent keepalive"
case allowedIPs = "Allowed IPs"
case excludePrivateIPs = "Exclude private IPs"
case deletePeer = "Delete peer"
enum PeerField {
case publicKey
case preSharedKey
case endpoint
case persistentKeepAlive
case allowedIPs
case excludePrivateIPs
case deletePeer
var localizedUIString: String {
switch self {
case .publicKey: return tr("tunnelPeerPublicKey")
case .preSharedKey: return tr("tunnelPeerPreSharedKey")
case .endpoint: return tr("tunnelPeerEndpoint")
case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
case .allowedIPs: return tr("tunnelPeerAllowedIPs")
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
case .deletePeer: return tr("deletePeerButtonTitle")
}
}
}
static let peerFieldsWithControl: Set<PeerField> = [
@@ -37,35 +63,31 @@ 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?
var validatedName: String?
subscript(field: InterfaceField) -> String {
get {
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.
if scratchpad.isEmpty {
populateScratchpad()
}
return scratchpad[field] ?? ""
}
set(stringValue) {
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.
if scratchpad.isEmpty {
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
validatedName = nil
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 {
@@ -76,13 +98,13 @@ class TunnelViewModel {
}
func populateScratchpad() {
// Populate the scratchpad from the configuration object
guard let config = validatedConfiguration else { return }
scratchpad[.name] = config.name
guard let name = validatedName else { return }
scratchpad[.name] = name
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if (!config.addresses.isEmpty) {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
}
if let listenPort = config.listenPort {
scratchpad[.listenPort] = String(listenPort)
@@ -90,40 +112,39 @@ class TunnelViewModel {
if let mtu = config.mtu {
scratchpad[.mtu] = String(mtu)
}
if (!config.dns.isEmpty) {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation() }.joined(separator: ", ")
if !config.dns.isEmpty {
scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
}
}
func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
return .saved(validatedConfiguration)
//swiftlint:disable:next cyclomatic_complexity function_body_length
func save() -> SaveResult<(String, InterfaceConfiguration)> {
if let config = validatedConfiguration, let name = validatedName {
return .saved((name, config))
}
fieldsWithError.removeAll()
guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
fieldsWithError.insert(.name)
return .error("Interface name is required")
return .error(tr("alertInvalidInterfaceMessageNameRequired"))
}
guard let privateKeyString = scratchpad[.privateKey] else {
fieldsWithError.insert(.privateKey)
return .error("Interface's private key is required")
return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
}
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")
return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
}
var config = InterfaceConfiguration(name: name, privateKey: privateKey)
var errorMessages: [String] = []
var config = InterfaceConfiguration(privateKey: privateKey)
var errorMessages = [String]()
if let addressesString = scratchpad[.addresses] {
var addresses: [IPAddressRange] = []
for addressString in addressesString.split(separator: ",") {
let trimmedString = addressString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let address = IPAddressRange(from: trimmedString) {
var addresses = [IPAddressRange]()
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let address = IPAddressRange(from: addressString) {
addresses.append(address)
} else {
fieldsWithError.insert(.addresses)
errorMessages.append("Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation")
errorMessages.append(tr("alertInvalidInterfaceMessageAddressInvalid"))
}
}
config.addresses = addresses
@@ -133,7 +154,7 @@ class TunnelViewModel {
config.listenPort = listenPort
} else {
fieldsWithError.insert(.listenPort)
errorMessages.append("Interface's listen port must be between 0 and 65535, or unspecified")
errorMessages.append(tr("alertInvalidInterfaceMessageListenPortInvalid"))
}
}
if let mtuString = scratchpad[.mtu] {
@@ -141,51 +162,49 @@ class TunnelViewModel {
config.mtu = mtu
} else {
fieldsWithError.insert(.mtu)
errorMessages.append("Interface's MTU must be between 576 and 65535, or unspecified")
errorMessages.append(tr("alertInvalidInterfaceMessageMTUInvalid"))
}
}
if let dnsString = scratchpad[.dns] {
var dnsServers: [DNSServer] = []
for dnsServerString in dnsString.split(separator: ",") {
let trimmedString = dnsServerString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let dnsServer = DNSServer(from: trimmedString) {
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let dnsServer = DNSServer(from: dnsServerString) {
dnsServers.append(dnsServer)
} else {
fieldsWithError.insert(.dns)
errorMessages.append("Interface's DNS servers must be a list of comma-separated IP addresses")
errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
}
}
config.dns = dnsServers
}
guard (errorMessages.isEmpty) else {
return .error(errorMessages.first!)
}
guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
validatedConfiguration = config
return .saved(config)
validatedName = name
return .saved((name, 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)
return !self[field].isEmpty
}
// TODO: Cache this to avoid recomputing
}
}
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 shouldStronglyRecommendDNS = false
private(set) var excludePrivateIPsValue = false
fileprivate var numberOfPeers = 0
init(index: Int) {
self.index = index
@@ -193,43 +212,38 @@ class TunnelViewModel {
subscript(field: PeerField) -> String {
get {
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.
if scratchpad.isEmpty {
populateScratchpad()
}
return scratchpad[field] ?? ""
}
set(stringValue) {
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.
if scratchpad.isEmpty {
populateScratchpad()
}
validatedConfiguration = nil
if (stringValue.isEmpty) {
if stringValue.isEmpty {
scratchpad.removeValue(forKey: field)
} else {
scratchpad[field] = stringValue
}
if (field == .allowedIPs) {
if field == .allowedIPs {
updateExcludePrivateIPsFieldState()
}
}
}
func populateScratchpad() {
// Populate the scratchpad from the configuration object
guard let config = validatedConfiguration else { return }
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if let preSharedKey = config.preSharedKey {
scratchpad[.preSharedKey] = preSharedKey.base64EncodedString()
}
if (!config.allowedIPs.isEmpty) {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
if !config.allowedIPs.isEmpty {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
}
if let endpoint = config.endpoint {
scratchpad[.endpoint] = endpoint.stringRepresentation()
scratchpad[.endpoint] = endpoint.stringRepresentation
}
if let persistentKeepAlive = config.persistentKeepAlive {
scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
@@ -237,39 +251,38 @@ class TunnelViewModel {
updateExcludePrivateIPsFieldState()
}
//swiftlint:disable:next cyclomatic_complexity
func save() -> SaveResult<PeerConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
return .saved(validatedConfiguration)
}
fieldsWithError.removeAll()
guard let publicKeyString = scratchpad[.publicKey] else {
fieldsWithError.insert(.publicKey)
return .error("Peer's public key is required")
return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
}
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")
return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
}
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)
errorMessages.append("Peer's preshared key must be a 32-byte key in base64 encoding")
errorMessages.append(tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
}
}
if let allowedIPsString = scratchpad[.allowedIPs] {
var allowedIPs: [IPAddressRange] = []
for allowedIPString in allowedIPsString.split(separator: ",") {
let trimmedString = allowedIPString.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if let allowedIP = IPAddressRange(from: trimmedString) {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
if let allowedIP = IPAddressRange(from: allowedIPString) {
allowedIPs.append(allowedIP)
} else {
fieldsWithError.insert(.allowedIPs)
errorMessages.append("Peer's allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation")
errorMessages.append(tr("alertInvalidPeerMessageAllowedIPsInvalid"))
}
}
config.allowedIPs = allowedIPs
@@ -279,7 +292,7 @@ class TunnelViewModel {
config.endpoint = endpoint
} else {
fieldsWithError.insert(.endpoint)
errorMessages.append("Peer's endpoint must be of the form 'host:port' or '[host]:port'")
errorMessages.append(tr("alertInvalidPeerMessageEndpointInvalid"))
}
}
if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
@@ -287,25 +300,23 @@ class TunnelViewModel {
config.persistentKeepAlive = persistentKeepAlive
} else {
fieldsWithError.insert(.persistentKeepAlive)
errorMessages.append("Peer's persistent keepalive must be between 0 to 65535, or unspecified")
errorMessages.append(tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
}
}
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)
}
// TODO: Cache this to avoid recomputing
}
static let ipv4DefaultRouteString = "0.0.0.0/0"
@@ -319,23 +330,20 @@ class TunnelViewModel {
]
func updateExcludePrivateIPsFieldState() {
guard (numberOfPeers == 1) else {
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
guard numberOfPeers == 1 else {
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false
return
}
if (scratchpad.isEmpty) {
populateScratchpad()
}
let allowedIPStrings = Set<String>(
(scratchpad[.allowedIPs] ?? "")
.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 {
@@ -345,42 +353,37 @@ class TunnelViewModel {
}
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) {
let allowedIPStrings = (scratchpad[.allowedIPs] ?? "")
.split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let dnsServerStrings = dnsServers
.split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .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
validatedConfiguration = nil
excludePrivateIPsValue = isOn
}
}
enum SaveResult<Configuration> {
case saved(Configuration)
case error(String) // TODO: Localize error messages
case error(String)
}
var interfaceData: InterfaceData
var peersData: [PeerData]
init(tunnelConfiguration: TunnelConfiguration?) {
let interfaceData: InterfaceData = InterfaceData()
var peersData: [PeerData] = []
let interfaceData = InterfaceData()
var peersData = [PeerData]()
if let tunnelConfiguration = tunnelConfiguration {
interfaceData.validatedConfiguration = tunnelConfiguration.interface
for (i, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: i)
interfaceData.validatedName = tunnelConfiguration.name
for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
let peerData = PeerData(index: index)
peerData.validatedConfiguration = peerConfiguration
peersData.append(peerData)
}
@@ -397,38 +400,36 @@ 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()
}
}
func save() -> SaveResult<TunnelConfiguration> {
// Attempt to save the interface and all peers, so that all erroring fields are collected
let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() }
// Collate the results
switch (interfaceSaveResult) {
let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
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):
@@ -438,35 +439,33 @@ class TunnelViewModel {
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if (peerPublicKeysArray.count != peerPublicKeysSet.count) {
return .error("Two or more peers cannot have the same public key")
if peerPublicKeysArray.count != peerPublicKeysSet.count {
return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
}
let tunnelConfiguration = TunnelConfiguration(interface: interfaceConfiguration, peers: peerConfigurations)
let tunnelConfiguration = TunnelConfiguration(name: interfaceConfiguration.0, interface: interfaceConfiguration.1, peers: peerConfigurations)
return .saved(tunnelConfiguration)
}
}
}
// MARK: Activate on demand
extension TunnelViewModel {
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch (activateOnDemandOption) {
switch activateOnDemandOption {
case .none:
return "Off"
return tr("tunnelOnDemandOptionOff")
case .useOnDemandOverWiFiOrCellular:
return "Wi-Fi or cellular"
return tr("tunnelOnDemandOptionWiFiOrCellular")
case .useOnDemandOverWiFiOnly:
return "Wi-Fi only"
return tr("tunnelOnDemandOptionWiFiOnly")
case .useOnDemandOverCellularOnly:
return "Cellular only"
return tr("tunnelOnDemandOptionCellularOnly")
}
}
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting {
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
if activateOnDemandSetting.isActivateOnDemandEnabled {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
@@ -479,5 +478,4 @@ extension TunnelViewModel {
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
return .useOnDemandOverWiFiOrCellular
}
}
+8 -16
View File
@@ -10,11 +10,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var mainVC: MainViewController?
func application(_ application: UIApplication,
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, 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,8 +27,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
mainVC?.tunnelsListVC?.importFromFile(url: url)
_ = FileManager.deleteFile(at: url)
mainVC?.tunnelsListVC?.importFromFile(url: url) {
_ = FileManager.deleteFile(at: url)
}
return true
}
@@ -37,8 +38,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
// MARK: State restoration
extension AppDelegate {
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
@@ -48,15 +47,9 @@ extension AppDelegate {
return true
}
func application(_ application: UIApplication,
viewControllerWithRestorationIdentifierPath identifierComponents: [String],
coder: NSCoder) -> UIViewController? {
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
guard let vcIdentifier = identifierComponents.last else { return nil }
if (vcIdentifier == "MainVC") {
return MainViewController()
} else if (vcIdentifier == "TunnelsListVC") {
return TunnelsListTableViewController()
} else if (vcIdentifier.hasPrefix("TunnelDetailVC:")) {
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) {
@@ -66,7 +59,6 @@ extension AppDelegate {
// 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,11 +5,15 @@ import UIKit
import os.log
class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?,
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = 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(title: String, message: String, from sourceVC: UIViewController?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
let (title, message) = error.alertText()
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)
@@ -17,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,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,257 +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"
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()
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 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: "No log available", message: "The pre-existing log could not be cleared", from: self)
return
}
}
guard let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL,
FileManager.default.fileExists(atPath: networkExtensionLogFileURL.path) else {
ErrorPresenter.showErrorAlert(title: "No log available", message: "Please activate a tunnel and then export the log", from: self)
return
}
do {
try FileManager.default.copyItem(at: networkExtensionLogFileURL, to: destinationURL)
} catch {
os_log("Failed to copy file: %{public}@ to %{public}@: %{public}@", log: OSLog.default, type: .error, networkExtensionLogFileURL.absoluteString, destinationURL.absoluteString, error.localizedDescription)
ErrorPresenter.showErrorAlert(title: "Log export failed", message: "The log could not be copied", 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 = 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 if (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
} else {
assert(field == .exportLogFile)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell
cell.buttonText = field.rawValue
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(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
}
}
@@ -1,436 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
// MARK: TunnelDetailTableViewController
class TunnelDetailTableViewController: UITableViewController {
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
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
super.init(style: .grouped)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = tunnelViewModel.interfaceData[.name]
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
self.tableView.rowHeight = 44
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(TunnelDetailTableViewActivateOnDemandCell.self, forCellReuseIdentifier: TunnelDetailTableViewActivateOnDemandCell.id)
// State restoration
self.restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
@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
self.present(alert, animated: true, completion: nil)
}
}
// MARK: TunnelEditTableViewControllerDelegate
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration())
self.title = tunnel.name
self.tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
// MARK: UITableViewDataSource
extension TunnelDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 4 + tunnelViewModel.peersData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let interfaceData = tunnelViewModel.interfaceData
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// 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]
return peerData.filterFieldsWithValueOrControl(peerFields: peerFields).count
} else if (section < (3 + numberOfPeerSections)) {
// Activate on demand
return 1
} else {
assert(section == (3 + numberOfPeerSections))
// Delete tunnel
return 1
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let numberOfPeerSections = tunnelViewModel.peersData.count
if (section == 0) {
// Status
return "Status"
} else if (section == 1) {
// Interface
return "Interface"
} else if ((numberOfPeerSections > 0) && (section < (2 + numberOfPeerSections))) {
// Peer
return "Peer"
} else if (section < (3 + numberOfPeerSections)) {
// On-Demand Activation
return "On-Demand Activation"
} else {
// Delete tunnel
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, onPresented: {
DispatchQueue.main.async {
cell.statusSwitch.isOn = false
}
})
}
}
} else {
s.tunnelsManager.startDeactivation(of: s.tunnel)
}
}
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 if (section < (3 + numberOfPeerSections)) {
// On-Demand Activation
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelDetailTableViewActivateOnDemandCell.id, for: indexPath) as! TunnelDetailTableViewActivateOnDemandCell
cell.tunnel = self.tunnel
return cell
} else {
assert(section == (3 + 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
}
}
}
class TunnelDetailTableViewStatusCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewStatusCell"
var tunnel: TunnelContainer? {
didSet(value) {
update(from: tunnel?.status)
statusObservervationToken = tunnel?.observe(\.status) { [weak self] (tunnel, _) in
self?.update(from: tunnel.status)
}
}
}
var isSwitchInteractionEnabled: Bool {
get { return statusSwitch.isUserInteractionEnabled }
set(value) { statusSwitch.isUserInteractionEnabled = value }
}
var onSwitchToggled: ((Bool) -> Void)?
private var isOnSwitchToggledHandlerEnabled: Bool = true
let statusSwitch: UISwitch
private var statusObservervationToken: AnyObject?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
statusSwitch = UISwitch()
super.init(style: .default, reuseIdentifier: TunnelDetailTableViewKeyValueCell.id)
accessoryView = statusSwitch
statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
}
@objc func switchToggled() {
if (isOnSwitchToggledHandlerEnabled) {
onSwitchToggled?(statusSwitch.isOn)
}
}
private func update(from status: TunnelStatus?) {
guard let status = status else {
reset()
return
}
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"
}
textLabel?.text = text
DispatchQueue.main.async { [weak statusSwitch] in
guard let statusSwitch = statusSwitch else { return }
statusSwitch.isOn = !(status == .deactivating || status == .inactive)
statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
}
textLabel?.textColor = (status == .active || status == .inactive) ? UIColor.black : UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func reset() {
textLabel?.text = "Invalid"
statusSwitch.isOn = false
textLabel?.textColor = UIColor.gray
statusSwitch.isUserInteractionEnabled = false
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}
class TunnelDetailTableViewKeyValueCell: CopyableLabelTableViewCell {
static let id: String = "TunnelDetailTableViewKeyValueCell"
var key: String {
get { return keyLabel.text ?? "" }
set(value) { keyLabel.text = value }
}
var value: String {
get { return valueLabel.text }
set(value) { valueLabel.text = value }
}
override var textToCopy: String? {
return self.valueLabel.text
}
let keyLabel: UILabel
let valueLabel: ScrollableLabel
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
keyLabel = UILabel()
valueLabel = ScrollableLabel()
keyLabel.textColor = UIColor.black
valueLabel.textColor = UIColor.gray
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(keyLabel)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
keyLabel.textAlignment = .left
NSLayoutConstraint.activate([
keyLabel.leftAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leftAnchor),
keyLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
])
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)
])
// 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)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
key = ""
value = ""
}
}
class TunnelDetailTableViewButtonCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewButtonCell"
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 TunnelDetailTableViewActivateOnDemandCell: UITableViewCell {
static let id: String = "TunnelDetailTableViewActivateOnDemandCell"
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"
}
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()
detailTextLabel?.text = ""
}
}
@@ -1,773 +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 activateOnDemandOptions: [ActivateOnDemandOption] = [
.useOnDemandOverWiFiOrCellular,
.useOnDemandOverWiFiOnly,
.useOnDemandOverCellularOnly
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting
init(tunnelsManager tm: TunnelsManager, tunnel t: TunnelContainer) {
// Use this initializer to edit an existing tunnel.
tunnelsManager = tm
tunnel = t
tunnelViewModel = TunnelViewModel(tunnelConfiguration: t.tunnelConfiguration())
activateOnDemandSetting = t.activateOnDemandSetting()
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)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting
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.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)
self.tableView.register(TunnelEditTableViewSelectionListCell.self, forCellReuseIdentifier: TunnelEditTableViewSelectionListCell.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,
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)
self.delegate?.tunnelEditingCancelled()
}
}
// MARK: UITableViewDataSource
extension TunnelEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
return numberOfInterfaceSections + numberOfPeerSections + 1 /* Add Peer */ + 1 /* On-Demand */
}
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 if (section < (numberOfInterfaceSections + numberOfPeerSections + 1)) {
// Add peer
return 1
} else {
// On-Demand Rules
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
return 4
} else {
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 if (section == (numberOfInterfaceSections + numberOfPeerSections)) {
// Add peer
return nil
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
return "On-Demand Activation"
}
}
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 if (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
} else {
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
if (row == 0) {
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSwitchCell.id, for: indexPath) as! TunnelEditTableViewSwitchCell
cell.message = "Activate on demand"
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] (isOn) in
guard let s = self else { return }
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
if (isOn) {
s.activateOnDemandSetting.isActivateOnDemandEnabled = true
if (s.activateOnDemandSetting.activateOnDemandOption == .none) {
s.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
}
s.tableView.insertRows(at: indexPaths, with: .automatic)
} else {
s.activateOnDemandSetting.isActivateOnDemandEnabled = false
s.tableView.deleteRows(at: indexPaths, with: .automatic)
}
}
return cell
} else {
assert(row < 4)
let cell = tableView.dequeueReusableCell(withIdentifier: TunnelEditTableViewSelectionListCell.id, for: indexPath) as! TunnelEditTableViewSelectionListCell
let rowOption = activateOnDemandOptions[row - 1]
let selectedOption = activateOnDemandSetting.activateOnDemandOption
assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = (selectedOption == rowOption)
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)
}
}
// MARK: UITableViewDelegate
extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
if (section == (numberOfInterfaceSections + numberOfPeerSections + 1)) {
return (row > 0) ? indexPath : nil
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let numberOfInterfaceSections = interfaceFieldsBySection.count
let numberOfPeerSections = tunnelViewModel.peersData.count
let section = indexPath.section
let row = indexPath.row
assert(section == (numberOfInterfaceSections + numberOfPeerSections + 1))
assert(row > 0)
let option = activateOnDemandOptions[row - 1]
assert(option != .none)
activateOnDemandSetting.activateOnDemandOption = option
let indexPaths: [IndexPath] = (1 ..< 4).map { IndexPath(row: $0, section: section) }
tableView.reloadRows(at: indexPaths, with: .automatic)
}
}
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
onValueBeingEdited = 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
}
}
class TunnelEditTableViewSelectionListCell: UITableViewCell {
static let id: String = "TunnelEditTableViewSelectionListCell"
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
}
}
@@ -1,445 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import MobileCoreServices
import UserNotifications
class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager?
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
// State restoration
self.restorationIdentifier = "TunnelsListVC"
}
func setTunnelsManager(tunnelsManager: TunnelsManager) {
if (self.tunnelsManager != nil) {
// If a tunnels manager is already set, do nothing
return
}
// Create the table view
let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.rowHeight = 60
tableView.separatorStyle = .none
tableView.register(TunnelsListTableViewCell.self, forCellReuseIdentifier: TunnelsListTableViewCell.id)
self.view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
tableView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
tableView.topAnchor.constraint(equalTo: self.view.topAnchor),
tableView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
tableView.dataSource = self
tableView.delegate = self
self.tableView = tableView
// Add button at the center
let centeredAddButton = BorderedTextButton()
centeredAddButton.title = "Add a tunnel"
centeredAddButton.isHidden = true
self.view.addSubview(centeredAddButton)
centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
centeredAddButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
centeredAddButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
])
centeredAddButton.onTapped = { [weak self] in
self?.addButtonTapped(sender: centeredAddButton)
}
centeredAddButton.isHidden = (tunnelsManager.numberOfTunnels() > 0)
self.centeredAddButton = centeredAddButton
// Hide the busy indicator
self.busyIndicator?.stopAnimating()
// Keep track of the tunnels manager
self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self
}
override func viewWillAppear(_: Bool) {
// Remove selection when getting back to the list view on iPhone
if let tableView = self.tableView, let selectedRowIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedRowIndexPath, animated: false)
}
}
@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 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 importFromFile(url: URL) {
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: [TunnelConfiguration?] = result.value!
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { [weak self] (numberSuccessful) in
if numberSuccessful == configs.count {
return
}
ErrorPresenter.showErrorAlert(title: "Created \(numberSuccessful) tunnels",
message: "Created \(numberSuccessful) of \(configs.count) tunnels from zip archive",
from: self)
}
}
} 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)
}
}
} else {
ErrorPresenter.showErrorAlert(title: "Unable to import tunnel",
message: "An error occured when importing the tunnel configuration.",
from: self)
}
}
}
}
// 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) { 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 = 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)
}
}
}
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", 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: 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(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.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
])
valueTextField.delegate = self
valueLabelScrollView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueTextField.leadingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leadingAnchor),
valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
valueTextField.trailingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.trailingAnchor),
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.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
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.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor)
]
isStackedVertically = true
isStackedHorizontally = false
}
} else {
// Stack horizontally
if !isStackedHorizontally {
constraints = [
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
valueLabelScrollView.leadingAnchor.constraint(equalToSystemSpacingAfter: keyLabel.trailingAnchor, 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,49 @@
// 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()
onSwitchToggled = nil
isEnabled = true
message = ""
isOn = false
}
}
@@ -0,0 +1,48 @@
// 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,
// 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")
}
override func prepareForReuse() {
super.prepareForReuse()
copyableGesture = false
}
}
@@ -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 ?? ""
nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
self?.nameLabel.text = tunnel.name
}
// Bind to the tunnel's status
update(from: tunnel?.status)
statusObservationToken = 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 statusObservationToken: AnyObject?
private var nameObservationToken: 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.trailingAnchor.constraint(equalTo: statusSwitch.trailingAnchor)
])
contentView.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, 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.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
busyIndicator.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, 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()
}
}
@@ -7,25 +7,24 @@ class MainViewController: UISplitViewController {
var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController?
private var foregroundObservationToken: AnyObject?
init() {
let detailVC = UIViewController()
detailVC.view.backgroundColor = UIColor.white
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
let masterVC = TunnelsListTableViewController()
let masterNC = UINavigationController(rootViewController: masterVC)
self.tunnelsListVC = masterVC
tunnelsListVC = masterVC
super.init(nibName: nil, bundle: nil)
self.viewControllers = [ masterNC, detailNC ]
viewControllers = [ masterNC, detailNC ]
// State restoration
self.restorationIdentifier = "MainVC"
restorationIdentifier = "MainVC"
masterNC.restorationIdentifier = "MasterNC"
detailNC.restorationIdentifier = "DetailNC"
}
@@ -35,35 +34,72 @@ class MainViewController: UISplitViewController {
}
override func viewDidLoad() {
self.delegate = self
delegate = self
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
self.preferredDisplayMode = .allVisible
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!
guard let s = self else { return }
s.tunnelsManager = tunnelsManager
s.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = s
tunnelsManager.activationDelegate = self
self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
}
foregroundObservationToken = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
self.tunnelsManager?.reload { [weak self] hasChanges in
guard let self = self, let tunnelsManager = self.tunnelsManager, hasChanges else { return }
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
if self.isCollapsed {
(self.viewControllers[0] as? UINavigationController)?.popViewController(animated: false)
} else {
let detailVC = UIViewController()
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
self.showDetailViewController(detailNC, sender: self)
}
if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
}
s.onTunnelsManagerReady?(tunnelsManager)
s.onTunnelsManagerReady = nil
}
}
}
extension MainViewController: TunnelsManagerActivationDelegate {
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError) {
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 {
@@ -74,13 +110,13 @@ extension MainViewController {
}
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) {
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] (tunnelsManager) in
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) {
if animated {
self.showDetailViewController(tunnelDetailNC, sender: self)
} else {
UIView.performWithoutAnimation {
@@ -2,7 +2,6 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import AVFoundation
import CoreData
import UIKit
protocol QRScanViewControllerDelegate: class {
@@ -18,29 +17,29 @@ 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 = tr("scanQRCodeViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
let tipLabel = UILabel()
tipLabel.text = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`"
tipLabel.text = tr("scanQRCodeTipText")
tipLabel.adjustsFontSizeToFitWidth = true
tipLabel.textColor = UIColor.lightGray
tipLabel.textColor = .lightGray
tipLabel.textAlignment = .center
view.addSubview(tipLabel)
tipLabel.translatesAutoresizingMaskIntoConstraints = false
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.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
tipLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
])
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
let captureSession = captureSession,
captureSession.canAddInput(videoInput),
captureSession.canAddOutput(metadataOutput) else {
scanDidEncounterError(title: "Camera Unsupported", message: "This device is not able to scan QR codes")
scanDidEncounterError(title: tr("alertScanQRCodeCameraUnsupportedTitle"), message: tr("alertScanQRCodeCameraUnsupportedMessage"))
return
}
@@ -77,15 +76,11 @@ class QRScanViewController: UIViewController {
super.viewDidLayoutSubviews()
if let connection = previewLayer?.connection {
let currentDevice: UIDevice = UIDevice.current
let orientation: UIDeviceOrientation = currentDevice.orientation
let previewLayerConnection: AVCaptureConnection = connection
let currentDevice = UIDevice.current
let orientation = currentDevice.orientation
let previewLayerConnection = connection
if previewLayerConnection.isVideoOrientationSupported {
switch orientation {
case .portrait:
previewLayerConnection.videoOrientation = .portrait
@@ -102,39 +97,38 @@ class QRScanViewController: UIViewController {
}
}
previewLayer?.frame = self.view.bounds
previewLayer?.frame = view.bounds
}
func scanDidComplete(withCode code: String) {
let scannedTunnelConfiguration = try? WgQuickConfigFileParser.parse(code, name: "Scanned")
let scannedTunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: code, called: "Scanned")
guard let tunnelConfiguration = scannedTunnelConfiguration else {
scanDidEncounterError(title: "Invalid QR Code", message: "The scanned QR code is not a valid WireGuard configuration")
scanDidEncounterError(title: tr("alertScanQRCodeInvalidQRCodeTitle"), message: tr("alertScanQRCodeInvalidQRCodeMessage"))
return
}
let alert = UIAlertController(title: NSLocalizedString("Please name the scanned tunnel", comment: ""), message: nil, preferredStyle: .alert)
let alert = UIAlertController(title: tr("alertScanQRCodeNamePromptTitle"), 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: tr("actionCancel"), 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 }
tunnelConfiguration.interface.name = title
if let s = self {
s.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: s) {
s.dismiss(animated: true, completion: nil)
})
alert.addAction(UIAlertAction(title: tr("actionSave"), style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
tunnelConfiguration.name = title
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: tr("actionOK"), style: .default) { [weak self] _ in
self?.dismiss(animated: true, completion: nil)
}))
})
present(alertController, animated: true)
captureSession = nil
}
@@ -151,7 +145,7 @@ extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate {
guard let metadataObject = metadataObjects.first,
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else {
scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read")
scanDidEncounterError(title: tr("alertScanQRCodeUnreadableQRCodeTitle"), message: tr("alertScanQRCodeUnreadableQRCodeMessage"))
return
}
@@ -0,0 +1,206 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class SettingsTableViewController: UITableViewController {
enum SettingsFields {
case iosAppVersion
case goBackendVersion
case exportZipArchive
case exportLogFile
var localizedUIString: String {
switch self {
case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
case .exportZipArchive: return tr("settingsExportZipButtonTitle")
case .exportLogFile: return tr("settingsExportLogFileButtonTitle")
}
}
}
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 = tr("settingsViewTitle")
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: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
return
}
}
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
return
}
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
return
}
DispatchQueue.main.async {
let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
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)
}
}
}
}
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 tr("settingsSectionTitleAbout")
case 1:
return tr("settingsSectionTitleExportConfigurations")
case 2:
return tr("settingsSectionTitleTunnelLog")
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.localizedUIString
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.localizedUIString
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.localizedUIString
cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button)
}
return cell
}
}
}
@@ -0,0 +1,255 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
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
]
let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
private var sections = [Section]()
private var onDemandStatusObservationToken: AnyObject?
private var statusObservationToken: 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")
}
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)
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: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
loadSections()
title = tunnel.name
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
tableView.reloadData()
}
func tunnelEditingCancelled() {
// Nothing to do
}
}
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 tr("tunnelSectionTitleStatus")
case .interface:
return tr("tunnelSectionTitleInterface")
case .peer:
return tr("tunnelSectionTitlePeer")
case .onDemand:
return tr("tunnelSectionTitleOnDemand")
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 = tr("tunnelStatusInactive")
case .activating:
text = tr("tunnelStatusActivating")
case .active:
text = tr("tunnelStatusActive")
case .deactivating:
text = tr("tunnelStatusDeactivating")
case .reasserting:
text = tr("tunnelStatusReasserting")
case .restarting:
text = tr("tunnelStatusRestarting")
case .waiting:
text = tr("tunnelStatusWaiting")
}
cell.textLabel?.text = text
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
}
cell.isEnabled = status == .active || status == .inactive
}
statusUpdate(cell, tunnel.status)
statusObservationToken = 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.localizedUIString
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.localizedUIString
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 = tr("tunnelOnDemandKey")
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting)
onDemandStatusObservationToken = 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 = tr("deleteTunnelButtonTitle")
cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in
guard let self = self else { return }
self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in
guard let self = self else { return }
self.tunnelsManager.remove(tunnel: self.tunnel) { error in
if error != nil {
print("Error removing tunnel: \(String(describing: error))")
return
}
}
if self.splitViewController?.isCollapsed != false {
self.navigationController?.navigationController?.popToRootViewController(animated: true)
} else {
let detailVC = UIViewController()
detailVC.view.backgroundColor = .white
let detailNC = UINavigationController(rootViewController: detailVC)
self.showDetailViewController(detailNC, sender: self)
}
}
}
return cell
}
}
@@ -0,0 +1,496 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import UIKit
protocol TunnelEditTableViewControllerDelegate: class {
func tunnelSaved(tunnel: TunnelContainer)
func tunnelEditingCancelled()
}
class TunnelEditTableViewController: UITableViewController {
private enum Section {
case interface
case peer(_ peer: TunnelViewModel.PeerData)
case addPeer
case onDemand
static func == (lhs: Section, rhs: Section) -> Bool {
switch (lhs, rhs) {
case (.interface, .interface),
(.addPeer, .addPeer),
(.onDemand, .onDemand):
return true
case let (.peer(peerA), .peer(peerB)):
return peerA.index == peerB.index
default:
return false
}
}
}
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]()
// Use this initializer to edit an existing tunnel.
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
activateOnDemandSetting = tunnel.activateOnDemandSetting
super.init(style: .grouped)
loadSections()
}
// Use this initializer to create a new tunnel.
init(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
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 ? tr("newTunnelViewTitle") : tr("editTunnelViewTitle")
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 alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil || tunnelViewModel.interfaceData.validatedName == nil) ?
tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle")
ErrorPresenter.showErrorAlert(title: alertTitle, 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 ? tr("tunnelSectionTitleInterface") : nil
case .peer:
return tr("tunnelSectionTitlePeer")
case .addPeer:
return nil
case .onDemand:
return tr("tunnelSectionTitleOnDemand")
}
}
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.localizedUIString
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.localizedUIString
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.localizedUIString
switch field {
case .name, .privateKey:
cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default
case .addresses:
cell.placeholderText = tr("tunnelEditPlaceholderTextStronglyRecommended")
cell.keyboardType = .numbersAndPunctuation
case .dns:
cell.placeholderText = tunnelViewModel.peersData.contains(where: { return $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation
case .listenPort, .mtu:
cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
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.localizedUIString
cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in
guard let self = self, let peerData = peerData else { return }
self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), 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.localizedUIString
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.localizedUIString
switch field {
case .publicKey:
cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
cell.keyboardType = .default
case .preSharedKey, .endpoint:
cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .default
case .allowedIPs:
cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
cell.keyboardType = .numbersAndPunctuation
case .persistentKeepAlive:
cell.placeholderText = tr("tunnelEditPlaceholderTextOff")
cell.keyboardType = .numberPad
case .excludePrivateIPs, .deletePeer:
cell.keyboardType = .default
}
cell.isValueValid = !peerData.fieldsWithError.contains(field)
cell.value = peerData[field]
if field == .allowedIPs {
let firstInterfaceSection = sections.firstIndex(where: { $0 == .interface })!
let interfaceSubSection = interfaceFieldsBySection.firstIndex(where: { $0.contains(.dns) })!
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex(where: { $0 == .dns })!
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, 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)
}
}
tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
}
} 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 = tr("addPeerButtonTitle")
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 = tr("tunnelOnDemandKey")
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return }
self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn
self.loadSections()
let section = self.sections.firstIndex(where: { $0 == .onDemand })!
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) }
if isOn {
if self.activateOnDemandSetting.activateOnDemandOption == .none {
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption()
}
self.tableView.insertRows(at: indexPaths, with: .fade)
} else {
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: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
}
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,294 @@
// 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 = tr("tunnelsListCenteredAddTunnelButtonTitle")
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.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
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 = tr("tunnelsListTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), 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) {
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: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet)
let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in
self?.presentViewControllerForFileImport()
}
alert.addAction(importFileAction)
let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in
self?.presentViewControllerForScanningQRCode()
}
alert.addAction(scanQRCodeAction)
let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in
if let self = self, let tunnelsManager = self.tunnelsManager {
self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager)
}
}
alert.addAction(createFromScratchAction)
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
alert.addAction(cancelAction)
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) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
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
}
let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful)
let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count)
ErrorPresenter.showErrorAlert(title: title, message: message, 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? TunnelConfiguration(fromWgQuickConfig: fileContents, called: 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: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"),
from: self, onPresented: completionHandler)
}
}
}
}
extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
urls.forEach {
importFromFile(url: $0, completionHandler: nil)
}
}
}
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?()
}
}
}
}
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
}
}
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: tr("tunnelsListSwipeDeleteButtonTitle")) { [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])
}
}
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
}
}
@@ -1,51 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import SystemConfiguration
class InternetReachability {
enum Status {
case unknown
case notReachable
case reachableOverWiFi
case reachableOverCellular
}
static func currentStatus() -> Status {
var status: Status = .unknown
if let reachabilityRef = InternetReachability.reachabilityRef() {
var flags = SCNetworkReachabilityFlags(rawValue: 0)
SCNetworkReachabilityGetFlags(reachabilityRef, &flags)
status = Status(reachabilityFlags: flags)
}
return status
}
private static func reachabilityRef() -> SCNetworkReachability? {
let addrIn = sockaddr_in(sin_len: UInt8(MemoryLayout<sockaddr_in>.size),
sin_family: sa_family_t(AF_INET),
sin_port: 0,
sin_addr: in_addr(s_addr: 0),
sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
return withUnsafePointer(to: addrIn) { (addrInPtr) -> SCNetworkReachability? in
addrInPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { (addrPtr) -> SCNetworkReachability? in
return SCNetworkReachabilityCreateWithAddress(nil, addrPtr)
}
}
}
}
extension InternetReachability.Status {
init(reachabilityFlags flags: SCNetworkReachabilityFlags) {
var status: InternetReachability.Status = .notReachable
if (flags.contains(.reachable)) {
if (flags.contains(.isWWAN)) {
status = .reachableOverCellular
} else {
status = .reachableOverWiFi
}
}
self = status
}
}
@@ -1,492 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Foundation
import NetworkExtension
import os.log
protocol TunnelsManagerListDelegate: class {
func tunnelAdded(at: Int)
func tunnelModified(at: Int)
func tunnelMoved(at oldIndex: Int, to newIndex: Int)
func tunnelRemoved(at: Int)
}
protocol TunnelsManagerActivationDelegate: class {
func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerError)
}
enum TunnelsManagerError: WireGuardAppError {
// Tunnels list management
case tunnelNameEmpty
case tunnelAlreadyExistsWithThatName
case vpnSystemErrorOnListingTunnels
case vpnSystemErrorOnAddTunnel
case vpnSystemErrorOnModifyTunnel
case vpnSystemErrorOnRemoveTunnel
// Tunnel activation
case tunnelActivationAttemptFailed // startTunnel() throwed
case tunnelActivationFailedInternalError // startTunnel() succeeded, but activation failed
case tunnelActivationFailedNoInternetConnection // startTunnel() succeeded, but activation failed since no internet
func alertText() -> (String, String) {
switch (self) {
case .tunnelNameEmpty:
return ("No name provided", "Can't create tunnel with an empty name")
case .tunnelAlreadyExistsWithThatName:
return ("Name already exists", "A tunnel with that name already exists")
case .vpnSystemErrorOnListingTunnels:
return ("Unable to list tunnels", "Internal error")
case .vpnSystemErrorOnAddTunnel:
return ("Unable to create tunnel", "Internal error")
case .vpnSystemErrorOnModifyTunnel:
return ("Unable to modify tunnel", "Internal error")
case .vpnSystemErrorOnRemoveTunnel:
return ("Unable to remove tunnel", "Internal error")
case .tunnelActivationAttemptFailed:
return ("Activation failure", "The tunnel could not be activated due to an internal error")
case .tunnelActivationFailedInternalError:
return ("Activation failure", "The tunnel could not be activated due to an internal error")
case .tunnelActivationFailedNoInternetConnection:
return ("Activation failure", "No internet connection")
}
}
}
class TunnelsManager {
private var tunnels: [TunnelContainer]
weak var tunnelsListDelegate: TunnelsManagerListDelegate?
weak var activationDelegate: TunnelsManagerActivationDelegate?
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 (WireGuardResult<TunnelsManager>) -> Void) {
#if targetEnvironment(simulator)
// NETunnelProviderManager APIs don't work on the simulator
completionHandler(.success(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)")
completionHandler(.failure(TunnelsManagerError.vpnSystemErrorOnListingTunnels))
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 self.tunnels.contains(where: { $0.name == tunnelName }) {
completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
return
}
isAddingTunnel = true
let tunnelProviderManager = NETunnelProviderManager()
tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
tunnelProviderManager.localizedDescription = tunnelName
tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager)
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(.failure(TunnelsManagerError.vpnSystemErrorOnAddTunnel))
return
}
if let s = self {
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
s.tunnels.append(tunnel)
s.tunnels.sort { $0.name < $1.name }
s.tunnelsListDelegate?.tunnelAdded(at: s.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()
self.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
}
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(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
return
}
oldName = tunnel.name
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
defer { self?.isModifyingTunnel = false }
guard (error == nil) else {
os_log("Modify: Saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelsManagerError.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.tunnelsListDelegate?.tunnelMoved(at: oldIndex, to: newIndex)
}
s.tunnelsListDelegate?.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()
}
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 {
os_log("Modify: Re-loading after saving configuration failed: %{public}@", log: OSLog.default, type: .error, "\(error!)")
completionHandler(TunnelsManagerError.vpnSystemErrorOnModifyTunnel)
return
}
completionHandler(nil)
}
} else {
completionHandler(nil)
}
}
}
}
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> 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(TunnelsManagerError.vpnSystemErrorOnRemoveTunnel)
return
}
if let s = self {
let index = s.tunnels.firstIndex(of: tunnel)!
s.tunnels.remove(at: index)
s.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 self.tunnels.first(where: { $0.name == tunnelName })
}
func startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
guard (tunnel.status == .inactive) else {
return
}
func _startActivation(of tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
tunnel.onActivationCommitted = { [weak self] (success) in
if (!success) {
let error = (InternetReachability.currentStatus() == .notReachable ?
TunnelsManagerError.tunnelActivationFailedNoInternetConnection :
TunnelsManagerError.tunnelActivationFailedInternalError)
self?.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: error)
}
}
tunnel.startActivation(completionHandler: completionHandler)
}
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
tunnel.status = .waiting
tunnelInOperation.onDeactivationComplete = {
_startActivation(of: tunnel, completionHandler: completionHandler)
}
startDeactivation(of: tunnelInOperation)
} else {
_startActivation(of: tunnel, completionHandler: completionHandler)
}
}
func startDeactivation(of tunnel: TunnelContainer) {
if (tunnel.status == .inactive) {
return
}
tunnel.startDeactivation()
}
func refreshStatuses() {
for t in tunnels {
t.refreshStatus()
}
}
}
class TunnelContainer: NSObject {
@objc dynamic var name: String
@objc dynamic var status: TunnelStatus
@objc dynamic var isActivateOnDemandEnabled: Bool {
didSet {
if (isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
}
var isAttemptingActivation: Bool = false
var onActivationCommitted: ((Bool) -> Void)?
var onDeactivationComplete: (() -> Void)?
fileprivate let tunnelProvider: NETunnelProviderManager
private var statusObservationToken: AnyObject?
init(tunnel: NETunnelProviderManager) {
self.name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status)
self.status = status
self.isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
self.tunnelProvider = tunnel
super.init()
if (status != .inactive || isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
func tunnelConfiguration() -> TunnelConfiguration? {
return (tunnelProvider.protocolConfiguration as! NETunnelProviderProtocol).tunnelConfiguration()
}
func activateOnDemandSetting() -> ActivateOnDemandSetting {
return ActivateOnDemandSetting(from: tunnelProvider)
}
func refreshStatus() {
let status = TunnelStatus(from: self.tunnelProvider.connection.status)
self.status = status
self.isActivateOnDemandEnabled = self.tunnelProvider.isOnDemandEnabled
if (status != .inactive || isActivateOnDemandEnabled) {
startObservingTunnelStatus()
}
}
fileprivate func startActivation(completionHandler: @escaping (TunnelsManagerError?) -> Void) {
assert(status == .inactive || status == .restarting || status == .waiting)
guard let tunnelConfiguration = tunnelConfiguration() else { fatalError() }
onDeactivationComplete = nil
isAttemptingActivation = true
startActivation(tunnelConfiguration: tunnelConfiguration, completionHandler: completionHandler)
}
fileprivate func startActivation(recursionCount: UInt = 0,
lastError: Error? = nil,
tunnelConfiguration: TunnelConfiguration,
completionHandler: @escaping (TunnelsManagerError?) -> Void) {
if (recursionCount >= 8) {
os_log("startActivation: Failed after 8 attempts. Giving up with %{public}@", log: OSLog.default, type: .error, "\(lastError!)")
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
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(TunnelsManagerError.tunnelActivationAttemptFailed)
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, completionHandler: completionHandler)
}
return
}
// Start the tunnel
startObservingTunnelStatus()
let session = (tunnelProvider.connection as! NETunnelProviderSession)
do {
os_log("startActivation: Starting tunnel", log: OSLog.default, type: .debug)
try session.startTunnel()
os_log("startActivation: Success", log: OSLog.default, type: .debug)
completionHandler(nil)
} catch (let error) {
guard let vpnError = error as? NEVPNError else {
os_log("Failed to activate tunnel: Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
guard (vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale) else {
os_log("Failed to activate tunnel: VPN Error: %{public}@", log: OSLog.default, type: .debug, "\(error)")
status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
return
}
assert(vpnError.code == NEVPNError.configurationInvalid || vpnError.code == NEVPNError.configurationStale)
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("startActivation: Error reloading tunnel: %{public}@", log: OSLog.default, type: .debug, "\(error!)")
self?.status = .inactive
completionHandler(TunnelsManagerError.tunnelActivationAttemptFailed)
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, 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.isAttemptingActivation) {
if (connection.status == .connecting || connection.status == .connected) {
// We tried to start the tunnel, and that attempt is on track to become succeessful
s.onActivationCommitted?(true)
s.onActivationCommitted = nil
} else if (connection.status == .disconnecting || connection.status == .disconnected) {
// We tried to start the tunnel, but that attempt didn't succeed
s.onActivationCommitted?(false)
s.onActivationCommitted = nil
}
s.isAttemptingActivation = false
}
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
}
s.status = TunnelStatus(from: connection.status)
if (s.status == .inactive) {
s.onDeactivationComplete?()
s.onDeactivationComplete = nil
if (!s.isActivateOnDemandEnabled) {
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 waiting // Waiting for another tunnel to be brought down
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"
+3 -1
View File
@@ -2,5 +2,7 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
protocol WireGuardAppError: Error {
func alertText() -> (/* title */ String, /* message */ String)
typealias AlertText = (title: String, message: String)
var alertText: AlertText { get }
}
+11 -11
View File
@@ -2,27 +2,27 @@
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
enum WireGuardResult<T> {
case success(T)
case failure(WireGuardAppError)
case success(_ value: T)
case failure(_ error: WireGuardAppError)
var value: T? {
switch (self) {
case .success(let v): return v
case .failure(_): return nil
switch self {
case .success(let value): return value
case .failure: return nil
}
}
var error: WireGuardAppError? {
switch (self) {
case .success(_): return nil
case .failure(let e): return e
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
switch self {
case .success: return true
case .failure: return false
}
}
}
-54
View File
@@ -68,10 +68,6 @@
#include <stdlib.h>
#include <string.h>
#ifndef NOUNCRYPT
#define NOUNCRYPT
#endif
#include "zlib.h"
#include "unzip.h"
@@ -185,18 +181,8 @@ typedef struct
int encrypted;
int isZip64;
# ifndef NOUNCRYPT
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
const z_crc_t* pcrc_32_tab;
# endif
} unz64_s;
#ifndef NOUNCRYPT
#include "crypt.h"
#endif
/* ===========================================================================
Read a byte from a gz_stream; update next_in and avail_in. Return EOF
for end of file.
@@ -1478,12 +1464,8 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
file_in_zip64_read_info_s* pfile_in_zip_read_info;
ZPOS64_T offset_local_extrafield; /* offset of the local extra field */
uInt size_local_extrafield; /* size of the local extra field */
# ifndef NOUNCRYPT
char source[12];
# else
if (password != NULL)
return UNZ_PARAMERROR;
# endif
if (file==NULL)
return UNZ_PARAMERROR;
@@ -1612,29 +1594,6 @@ extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
s->pfile_in_zip_read = pfile_in_zip_read_info;
s->encrypted = 0;
# ifndef NOUNCRYPT
if (password != NULL)
{
int i;
s->pcrc_32_tab = get_crc_table();
init_keys(password,s->keys,s->pcrc_32_tab);
if (ZSEEK64(s->z_filefunc, s->filestream,
s->pfile_in_zip_read->pos_in_zipfile +
s->pfile_in_zip_read->byte_before_the_zipfile,
SEEK_SET)!=0)
return UNZ_INTERNALERROR;
if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12)
return UNZ_INTERNALERROR;
for (i = 0; i<12; i++)
zdecode(s->keys,s->pcrc_32_tab,source[i]);
s->pfile_in_zip_read->pos_in_zipfile+=12;
s->encrypted=1;
}
# endif
return UNZ_OK;
}
@@ -1739,19 +1698,6 @@ extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len)
uReadThis)!=uReadThis)
return UNZ_ERRNO;
# ifndef NOUNCRYPT
if(s->encrypted)
{
uInt i;
for(i=0;i<uReadThis;i++)
pfile_in_zip_read_info->read_buffer[i] =
zdecode(s->keys,s->pcrc_32_tab,
pfile_in_zip_read_info->read_buffer[i]);
}
# endif
pfile_in_zip_read_info->pos_in_zipfile += uReadThis;
pfile_in_zip_read_info->rest_read_compressed-=uReadThis;
-49
View File
@@ -153,11 +153,6 @@ typedef struct
ZPOS64_T pos_zip64extrainfo;
ZPOS64_T totalCompressedData;
ZPOS64_T totalUncompressedData;
#ifndef NOCRYPT
unsigned long keys[3]; /* keys defining the pseudo-random sequence */
const z_crc_t* pcrc_32_tab;
int crypt_header_size;
#endif
} curfile64_info;
typedef struct
@@ -178,12 +173,6 @@ typedef struct
} zip64_internal;
#ifndef NOCRYPT
#define INCLUDECRYPTINGCODE_IFCRYPTALLOWED
#include "crypt.h"
#endif
local linkedlist_datablock_internal* allocate_new_datablock()
{
linkedlist_datablock_internal* ldi;
@@ -1064,12 +1053,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
uInt i;
int err = ZIP_OK;
# ifdef NOCRYPT
(void)(crcForCrypting);
if (password != NULL)
return ZIP_PARAMERROR;
# endif
if (file == NULL)
return ZIP_PARAMERROR;
@@ -1237,24 +1220,6 @@ extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename,
}
# ifndef NOCRYPT
zi->ci.crypt_header_size = 0;
if ((err==Z_OK) && (password != NULL))
{
unsigned char bufHead[RAND_HEAD_LEN];
unsigned int sizeHead;
zi->ci.encrypt = 1;
zi->ci.pcrc_32_tab = get_crc_table();
/*init_keys(password,zi->ci.keys,zi->ci.pcrc_32_tab);*/
sizeHead=crypthead(password,bufHead,RAND_HEAD_LEN,zi->ci.keys,zi->ci.pcrc_32_tab,crcForCrypting);
zi->ci.crypt_header_size = sizeHead;
if (ZWRITE64(zi->z_filefunc,zi->filestream,bufHead,sizeHead) != sizeHead)
err = ZIP_ERRNO;
}
# endif
if (err==Z_OK)
zi->in_opened_file_inzip = 1;
return err;
@@ -1362,16 +1327,6 @@ local int zip64FlushWriteBuffer(zip64_internal* zi)
{
int err=ZIP_OK;
if (zi->ci.encrypt != 0)
{
#ifndef NOCRYPT
uInt i;
int t;
for (i=0;i<zi->ci.pos_in_buffered_data;i++)
zi->ci.buffered_data[i] = zencode(zi->ci.keys, zi->ci.pcrc_32_tab, zi->ci.buffered_data[i],t);
#endif
}
if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
err = ZIP_ERRNO;
@@ -1602,10 +1557,6 @@ extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_s
}
compressed_size = zi->ci.totalCompressedData;
# ifndef NOCRYPT
compressed_size += zi->ci.crypt_header_size;
# endif
// update Current Item crc and sizes,
if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
{
+22 -30
View File
@@ -8,14 +8,14 @@ enum ZipArchiveError: WireGuardAppError {
case cantOpenOutputZipFileForWriting
case badArchive
func alertText() -> (String, String) {
switch (self) {
var alertText: AlertText {
switch self {
case .cantOpenInputZipFile:
return ("Unable to read zip archive", "The zip archive could not be read.")
return (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage"))
case .cantOpenOutputZipFileForWriting:
return ("Unable to create zip archive", "Could not open zip file for writing.")
return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
case .badArchive:
return ("Unable to read zip archive", "Bad or corrupt zip archive.")
return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
}
}
}
@@ -39,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
@@ -49,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 = 16384 // 16 kb
let bufferSize = 16384 // 16 KiB
var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
@@ -68,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
@@ -6,19 +6,15 @@ import UIKit
enum ZipExporterError: WireGuardAppError {
case noTunnelsToExport
func alertText() -> (String, String) {
switch (self) {
case .noTunnelsToExport:
return ("Nothing to export", "There are no tunnels to export")
}
var alertText: AlertText {
return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage"))
}
}
class ZipExporter {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL,
completion: @escaping (WireGuardAppError?) -> Void) {
static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
guard (!tunnelConfigurations.isEmpty) else {
guard !tunnelConfigurations.isEmpty else {
completion(ZipExporterError.noTunnelsToExport)
return
}
@@ -26,16 +22,16 @@ class ZipExporter {
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 }
if let contents = tunnelConfiguration.asWgQuickConfig().data(using: .utf8) {
let name = tunnelConfiguration.name ?? "untitled"
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) {
} catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(error) }
return
} catch {
@@ -6,53 +6,46 @@ import UIKit
enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive
func alertText() -> (String, String) {
switch (self) {
case .noTunnelsInZipArchive:
return ("No tunnels in zip archive", "No .conf tunnel files were found inside the zip archive.")
}
var alertText: AlertText {
return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
}
}
class ZipImporter {
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
var unarchivedFiles: [(fileName: String, contents: Data)]
var unarchivedFiles: [(fileBaseName: String, contents: Data)]
do {
unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
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
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: i)
unarchivedFiles.remove(at: index)
}
}
if (unarchivedFiles.isEmpty) {
if unarchivedFiles.isEmpty {
throw ZipImporterError.noTunnelsInZipArchive
}
} catch (let error as WireGuardAppError) {
} catch let error as WireGuardAppError {
DispatchQueue.main.async { completion(.failure(error)) }
return
} catch {
fatalError()
}
unarchivedFiles.sort { $0.fileName < $1.fileName }
var configs = Array<TunnelConfiguration?>(repeating: nil, count: unarchivedFiles.count)
for (i, file) in unarchivedFiles.enumerated() {
if (i > 0 && file == unarchivedFiles[i - 1]) {
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.fileName) else {
continue
}
configs[i] = tunnelConfig
guard let fileContents = String(data: file.contents, encoding: .utf8) else { continue }
guard let tunnelConfig = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: file.fileBaseName) else { continue }
configs[index] = tunnelConfig
}
DispatchQueue.main.async { completion(.success(configs)) }
}
@@ -4,37 +4,33 @@
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()) {
if !endpoint.hasHostAsIPAddress() {
return false
}
}
return true
}
static func resolveSync(endpoints: [Endpoint?]) throws -> [Endpoint?] {
let dispatchGroup: DispatchGroup = DispatchGroup()
static func resolveSync(endpoints: [Endpoint?]) -> [Endpoint?]? {
let dispatchGroup = DispatchGroup()
if (isAllEndpointsAlreadyResolved(endpoints: endpoints)) {
if isAllEndpointsAlreadyResolved(endpoints: endpoints) {
return endpoints
}
var resolvedEndpoints: [Endpoint?] = Array<Endpoint?>(repeating: nil, count: endpoints.count)
for (i, endpoint) in endpoints.enumerated() {
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[i] = endpoint
if endpoint.hasHostAsIPAddress() {
resolvedEndpoints[index] = endpoint
} else {
let workItem = DispatchWorkItem {
resolvedEndpoints[i] = DNSResolver.resolveSync(endpoint: endpoint)
resolvedEndpoints[index] = DNSResolver.resolveSync(endpoint: endpoint)
}
DispatchQueue.global(qos: .userInitiated).async(group: dispatchGroup, execute: workItem)
}
@@ -42,92 +38,115 @@ class DNSResolver {
dispatchGroup.wait() // TODO: Timeout?
var hostnamesWithDnsResolutionFailure: [String] = []
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
if resolvedEndpoint == nil {
guard let hostname = endpoint.hostname() else { fatalError() }
hostnamesWithDnsResolutionFailure.append(hostname)
}
}
}
if (!hostnamesWithDnsResolutionFailure.isEmpty) {
throw DNSResolverError.dnsResolutionFailed(hostnames: hostnamesWithDnsResolutionFailure)
if !hostnamesWithDnsResolutionFailure.isEmpty {
wg_log(.error, message: "DNS resolution failed for the following hostnames: \(hostnamesWithDnsResolutionFailure.joined(separator: ", "))")
return nil
}
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
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) {
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
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo(
ai_flags: AI_ALL, // We set this to ALL so that we get v4 addresses even on DNS64 networks
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
if getaddrinfo(name, "\(endpoint.port)", &hints, &resultPointer) != 0 {
return nil
}
var next = resultPointer
var ipv4Address: IPv4Address?
var ipv6Address: IPv6Address?
while next != nil {
let result = next!.pointee
next = 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
ipv4Address = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
break // If we found an IPv4 address, we can stop
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
ipv6Address = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
continue // If we already have an IPv6 address, we can skip this one
}
}
freeaddrinfo(resultPointer)
// We prefer an IPv4 address over an IPv6 address
if let ipv4Address = ipv4Address {
return Endpoint(host: .ipv4(ipv4Address), port: endpoint.port)
} else if let ipv6Address = ipv6Address {
return Endpoint(host: .ipv6(ipv6Address), port: endpoint.port)
} else {
// getaddrinfo failed
return nil
}
default:
// The endpoint is already resolved
return endpoint
}
}
}
extension Endpoint {
func withReresolvedIP() -> Endpoint {
var ret = self
let hostname: String
switch host {
case .name(let name, _):
hostname = name
case .ipv4(let address):
hostname = "\(address)"
case .ipv6(let address):
hostname = "\(address)"
}
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
var hints = addrinfo(
ai_flags: 0, // We set this to zero so that we actually resolve this using DNS64
ai_family: AF_UNSPEC,
ai_socktype: SOCK_DGRAM,
ai_protocol: IPPROTO_UDP,
ai_addrlen: 0,
ai_canonname: nil,
ai_addr: nil,
ai_next: nil)
if getaddrinfo(hostname, "\(port)", &hints, &resultPointer) != 0 || resultPointer == nil {
return ret
}
let result = resultPointer!.pointee
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
let addr = IPv4Address(Data(bytes: &sa4.sin_addr, count: MemoryLayout<in_addr>.size))
ret = Endpoint(host: .ipv4(addr!), port: port)
} else if result.ai_family == AF_INET6 && result.ai_addrlen == MemoryLayout<sockaddr_in6>.size {
var sa6 = UnsafeRawPointer(result.ai_addr)!.assumingMemoryBound(to: sockaddr_in6.self).pointee
let addr = IPv6Address(Data(bytes: &sa6.sin6_addr, count: MemoryLayout<in6_addr>.size))
ret = Endpoint(host: .ipv6(addr!), port: port)
}
freeaddrinfo(resultPointer)
if ret.host != host {
wg_log(.debug, message: "DNS64: mapped \(host) to \(ret.host)")
} else {
wg_log(.debug, message: "DNS64: mapped \(host) to itself.")
}
return ret
}
}
@@ -4,22 +4,22 @@
import NetworkExtension
class ErrorNotifier {
static 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")
}
let activationAttemptId: String?
init(activationAttemptId: String?) {
self.activationAttemptId = activationAttemptId
ErrorNotifier.removeLastErrorFile()
}
static func notify(_ error: PacketTunnelProviderError, from tunnelProvider: NEPacketTunnelProvider) {
guard let (title, message) = ErrorNotifier.errorMessage(for: error) else { return }
// displayMessage() is deprecated, but there's no better alternative to show the error to the user
tunnelProvider.displayMessage("\(title): \(message)", completionHandler: { (_) in })
func notify(_ error: PacketTunnelProviderError) {
guard let activationAttemptId = activationAttemptId, let lastErrorFilePath = FileManager.networkExtensionLastErrorFileURL?.path else { return }
let errorMessageData = "\(activationAttemptId)\n\(error)".data(using: .utf8)
FileManager.default.createFile(atPath: lastErrorFilePath, contents: errorMessageData, attributes: nil)
}
static func removeLastErrorFile() {
if let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL {
_ = FileManager.deleteFile(at: lastErrorFileURL)
}
}
}
@@ -1,144 +1,103 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import NetworkExtension
import Foundation
import Network
import NetworkExtension
import os.log
enum PacketTunnelProviderError: Error {
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure(hostnames: [String])
case couldNotStartWireGuard
case coultNotSetNetworkSettings
}
private var logFileHandle: FileHandle?
/// A packet tunnel provider object.
class PacketTunnelProvider: NEPacketTunnelProvider {
// MARK: Properties
private var handle: Int32?
private var networkMonitor: NWPathMonitor?
private var ifname: String?
private var packetTunnelSettingsGenerator: PacketTunnelSettingsGenerator?
private var wgHandle: Int32?
deinit {
networkMonitor?.cancel()
}
// MARK: NEPacketTunnelProvider
//swiftlint:disable:next function_body_length
override func startTunnel(options: [String: NSObject]?, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
let activationAttemptId = options?["activationAttemptId"] as? String
let errorNotifier = ErrorNotifier(activationAttemptId: activationAttemptId)
/// Begin the process of establishing the tunnel.
override func startTunnel(options: [String: NSObject]?,
completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
guard let tunnelProviderProtocol = self.protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.tunnelConfiguration() else {
ErrorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid, from: self)
guard let tunnelProviderProtocol = protocolConfiguration as? NETunnelProviderProtocol,
let tunnelConfiguration = tunnelProviderProtocol.asTunnelConfiguration() else {
errorNotifier.notify(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
startTunnelCompletionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
startTunnel(with: tunnelConfiguration, completionHandler: startTunnelCompletionHandler)
}
func startTunnel(with tunnelConfiguration: TunnelConfiguration, completionHandler startTunnelCompletionHandler: @escaping (Error?) -> Void) {
// Configure logging
configureLogger()
wg_log(.info, message: "WireGuard for iOS version \(appVersion())")
wg_log(.info, message: "WireGuard Go backend version \(goBackendVersion())")
wg_log(.info, message: "Tunnel interface name: \(tunnelConfiguration.interface.name)")
wg_log(.info, staticMessage: "Starting tunnel")
// Resolve endpoint domains
wg_log(.info, message: "Starting tunnel from the " + (activationAttemptId == nil ? "OS directly, rather than the app" : "app"))
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), from: self)
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure(hostnames: hostnames))
guard let resolvedEndpoints = DNSResolver.resolveSync(endpoints: endpoints) else {
errorNotifier.notify(PacketTunnelProviderError.dnsResolutionFailure)
startTunnelCompletionHandler(PacketTunnelProviderError.dnsResolutionFailure)
return
} catch {
// There can be no other errors from DNSResolver.resolveSync()
fatalError()
}
assert(endpoints.count == resolvedEndpoints.count)
// Setup packetTunnelSettingsGenerator
packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration, resolvedEndpoints: resolvedEndpoints)
let packetTunnelSettingsGenerator = PacketTunnelSettingsGenerator(tunnelConfiguration: tunnelConfiguration,
resolvedEndpoints: resolvedEndpoints)
// Bring up wireguard-go backend
let fd = packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32
if fd < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
let wireguardSettings = packetTunnelSettingsGenerator.generateWireGuardSettings()
let handle = connect(interfaceName: tunnelConfiguration.interface.name, settings: wireguardSettings, fd: fd)
if handle < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not start WireGuard")
ErrorNotifier.notify(PacketTunnelProviderError.couldNotStartWireGuard, from: self)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartWireGuard)
return
}
wgHandle = handle
// Apply network settings
let networkSettings: NEPacketTunnelNetworkSettings = packetTunnelSettingsGenerator.generateNetworkSettings()
setTunnelNetworkSettings(networkSettings) { (error) in
setTunnelNetworkSettings(packetTunnelSettingsGenerator!.generateNetworkSettings()) { error in
if let error = error {
wg_log(.error, staticMessage: "Starting tunnel failed: Error setting network settings.")
wg_log(.error, message: "Error from setTunnelNetworkSettings: \(error.localizedDescription)")
ErrorNotifier.notify(PacketTunnelProviderError.coultNotSetNetworkSettings, from: self)
startTunnelCompletionHandler(PacketTunnelProviderError.coultNotSetNetworkSettings)
wg_log(.error, message: "Starting tunnel failed with setTunnelNetworkSettings returning \(error.localizedDescription)")
errorNotifier.notify(PacketTunnelProviderError.couldNotSetNetworkSettings)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotSetNetworkSettings)
} else {
startTunnelCompletionHandler(nil /* No errors */)
self.networkMonitor = NWPathMonitor()
self.networkMonitor!.pathUpdateHandler = self.pathUpdate
self.networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
let fileDescriptor = (self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32) ?? -1
if fileDescriptor < 0 {
wg_log(.error, staticMessage: "Starting tunnel failed: Could not determine file descriptor")
errorNotifier.notify(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotDetermineFileDescriptor)
return
}
var ifnameSize = socklen_t(IFNAMSIZ)
let ifnamePtr = UnsafeMutablePointer<CChar>.allocate(capacity: Int(ifnameSize))
ifnamePtr.initialize(repeating: 0, count: Int(ifnameSize))
if getsockopt(fileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, ifnamePtr, &ifnameSize) == 0 {
self.ifname = String(cString: ifnamePtr)
}
ifnamePtr.deallocate()
wg_log(.info, message: "Tunnel interface is \(self.ifname ?? "unknown")")
let handle = self.packetTunnelSettingsGenerator!.uapiConfiguration().withGoString { return wgTurnOn($0, fileDescriptor) }
if handle < 0 {
wg_log(.error, message: "Starting tunnel failed with wgTurnOn returning \(handle)")
errorNotifier.notify(PacketTunnelProviderError.couldNotStartBackend)
startTunnelCompletionHandler(PacketTunnelProviderError.couldNotStartBackend)
return
}
self.handle = handle
startTunnelCompletionHandler(nil)
}
}
}
/// Begin the process of stopping the tunnel.
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
networkMonitor?.cancel()
networkMonitor = nil
ErrorNotifier.removeLastErrorFile()
wg_log(.info, staticMessage: "Stopping tunnel")
if let handle = wgHandle {
if let handle = handle {
wgTurnOff(handle)
}
if let fileHandle = logFileHandle {
fileHandle.closeFile()
}
completionHandler()
}
private func configureLogger() {
// Setup writing the log to a file
if let networkExtensionLogFileURL = FileManager.networkExtensionLogFileURL {
let fileManager = FileManager.default
let filePath = networkExtensionLogFileURL.path
fileManager.createFile(atPath: filePath, contents: nil) // Create the file if it doesn't already exist
if let fileHandle = FileHandle(forWritingAtPath: filePath) {
logFileHandle = fileHandle
} else {
os_log("Can't open log file for writing. Log is not saved to file.", log: OSLog.default, type: .error)
logFileHandle = nil
}
} else {
os_log("Can't obtain log file URL. Log is not saved to file.", log: OSLog.default, type: .error)
}
// Setup WireGuard logger
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:
@@ -150,67 +109,29 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
default:
logType = .default
}
let msg = (msgCStr != nil) ? String(cString: msgCStr!) : ""
wg_log(logType, message: 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 pathUpdate(path: Network.NWPath) {
guard let handle = handle, let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator else { return }
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
var interfaces = path.availableInterfaces
if let ifname = ifname {
interfaces = interfaces.filter { $0.name != ifname }
}
}
func appVersion() -> String {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
return appVersion
}
func goBackendVersion() -> String {
return WIREGUARD_GO_VERSION
}
}
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)
if let ifscope = interfaces.first?.index {
wgBindInterfaceScope(handle, Int32(ifscope))
}
}
}
private func wg_log(_ type: OSLogType, staticMessage msg: StaticString) {
// Write to os log
os_log(msg, log: OSLog.default, type: type)
// Write to file log
let msgString: String = msg.withUTF8Buffer { (ptr: UnsafeBufferPointer<UInt8>) -> String in
return String(decoding: ptr, as: UTF8.self)
}
file_log(type: type, message: msgString)
}
private func wg_log(_ type: OSLogType, message msg: String) {
// Write to os log
os_log("%{public}s", log: OSLog.default, type: type, msg)
// Write to file log
file_log(type: type, message: msg)
}
private func file_log(type: OSLogType, message: String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS: "
var msgLine = formatter.string(from: Date()) + message
if (msgLine.last! != "\n") {
msgLine.append("\n")
}
let data = msgLine.data(using: .utf8)
if let data = data, let logFileHandle = logFileHandle {
logFileHandle.write(data)
logFileHandle.synchronizeFile()
extension String {
func withGoString<R>(_ call: (gostring_t) -> R) -> R {
func helper(_ pointer: UnsafePointer<Int8>?, _ call: (gostring_t) -> R) -> R {
return call(gostring_t(p: pointer, n: utf8.count))
}
return helper(self, call)
}
}
@@ -6,7 +6,6 @@ import Network
import NetworkExtension
class PacketTunnelSettingsGenerator {
let tunnelConfiguration: TunnelConfiguration
let resolvedEndpoints: [Endpoint?]
@@ -15,187 +14,130 @@ class PacketTunnelSettingsGenerator {
self.resolvedEndpoints = resolvedEndpoints
}
func generateWireGuardSettings() -> String {
func endpointUapiConfiguration() -> String {
var wgSettings = ""
for (index, peer) in tunnelConfiguration.peers.enumerated() {
wgSettings.append("public_key=\(peer.publicKey.hexEncodedString())\n")
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
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) {
if !tunnelConfiguration.peers.isEmpty {
wgSettings.append("replace_peers=true\n")
}
assert(tunnelConfiguration.peers.count == resolvedEndpoints.count)
for (i, peer) in tunnelConfiguration.peers.enumerated() {
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[i] {
if let endpoint = resolvedEndpoints[index]?.withReresolvedIP() {
if case .name(_, _) = endpoint.host { assert(false, "Endpoint is not resolved") }
wgSettings.append("endpoint=\(endpoint.stringRepresentation())\n")
wgSettings.append("endpoint=\(endpoint.stringRepresentation)\n")
}
let persistentKeepAlive = peer.persistentKeepAlive ?? 0
wgSettings.append("persistent_keepalive_interval=\(persistentKeepAlive)\n")
if (!peer.allowedIPs.isEmpty) {
if !peer.allowedIPs.isEmpty {
wgSettings.append("replace_allowed_ips=true\n")
for ip in peer.allowedIPs {
wgSettings.append("allowed_ip=\(ip.stringRepresentation())\n")
}
peer.allowedIPs.forEach { wgSettings.append("allowed_ip=\($0.stringRepresentation)\n") }
}
}
return wgSettings
}
func generateNetworkSettings() -> NEPacketTunnelNetworkSettings {
// 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
}
}
let remoteAddress = "0.0.0.0"
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress)
// DNS
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation() }
let dnsServerStrings = tunnelConfiguration.interface.dns.map { $0.stringRepresentation }
let dnsSettings = NEDNSSettings(servers: dnsServerStrings)
dnsSettings.matchDomains = [""] // All DNS queries must first go through the VPN's DNS
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS
networkSettings.dnsSettings = dnsSettings
// MTU
let mtu = tunnelConfiguration.interface.mtu ?? 0
if (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)
}
// Addresses from interface addresses
let (ipv4Routes, ipv6Routes) = routes()
let (ipv4IncludedRoutes, ipv6IncludedRoutes) = includedRoutes()
var ipv4Addresses: [String] = []
var ipv4SubnetMasks: [String] = []
var ipv6Addresses: [String] = []
var ipv6NetworkPrefixLengths: [NSNumber] = []
for addressRange in tunnelConfiguration.interface.addresses {
if (addressRange.address is IPv4Address) {
ipv4Addresses.append("\(addressRange.address)")
ipv4SubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6Addresses.append("\(addressRange.address)")
ipv6NetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
// Included routes from AllowedIPs
var ipv4IncludedRouteAddresses: [String] = []
var ipv4IncludedRouteSubnetMasks: [String] = []
var ipv6IncludedRouteAddresses: [String] = []
var ipv6IncludedRouteNetworkPrefixLengths: [NSNumber] = []
for peer in tunnelConfiguration.peers {
for addressRange in peer.allowedIPs {
if (addressRange.address is IPv4Address) {
ipv4IncludedRouteAddresses.append("\(addressRange.address)")
ipv4IncludedRouteSubnetMasks.append(PacketTunnelSettingsGenerator.ipv4SubnetMaskString(of: addressRange))
} else if (addressRange.address is IPv6Address) {
ipv6IncludedRouteAddresses.append("\(addressRange.address)")
ipv6IncludedRouteNetworkPrefixLengths.append(NSNumber(value: addressRange.networkPrefixLength))
}
}
}
// 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()
}
}
// Apply 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)
}
let ipv4Settings = NEIPv4Settings(addresses: ipv4Routes.map { $0.destinationAddress }, subnetMasks: ipv4Routes.map { $0.destinationSubnetMask })
ipv4Settings.includedRoutes = ipv4IncludedRoutes
networkSettings.ipv4Settings = ipv4Settings
// Apply 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)
}
let ipv6Settings = NEIPv6Settings(addresses: ipv6Routes.map { $0.destinationAddress }, networkPrefixLengths: ipv6Routes.map { $0.destinationNetworkPrefixLength })
ipv6Settings.includedRoutes = ipv6IncludedRoutes
networkSettings.ipv6Settings = ipv6Settings
// Done
return networkSettings
}
static func ipv4SubnetMaskString(of addressRange: IPAddressRange) -> String {
let n: UInt8 = addressRange.networkPrefixLength
assert(n <= 32)
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 = n > 0 ? ~UInt32(0) << (32 - n) : UInt32(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 extension Data {
@@ -1,2 +1,3 @@
#include "../../wireguard-go-bridge/wireguard.h"
#include "wireguard-go-version.h"
#include "ringlogger.h"
+20 -26
View File
@@ -2,24 +2,24 @@
#
# Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
# These are generally passed to us by xcode, but we set working defaults for standalone compilation too.
ARCHS ?= arm64 armv7
SDK_NAME ?= iphoneos
SDKROOT ?= $(shell xcrun --sdk $(SDK_NAME) --show-sdk-path)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
export CC ?= clang
LIPO ?= lipo
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum
DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go)
ARCHS ?= arm64 armv7 x86_64
CFLAGS_PREFIX := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)) -isysroot $(SDKROOT) -arch
GOARCH_arm64 := arm64
GOARCH_armv7 := arm
GOARCH_x86_64 := amd64
DEPLOYMENT_CFLAGS := $(DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX)$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME))
CGO_FLAGS_x86_64 := -arch x86_64 -isysroot $(shell xcrun --sdk iphonesimulator --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CGO_FLAGS_arm64 := -arch arm64 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CGO_FLAGS_armv7 := -arch armv7 -isysroot $(shell xcrun --sdk iphoneos --show-sdk-path) $(DEPLOYMENT_CFLAGS)
CC_x86_64 := $(shell xcrun --sdk iphonesimulator --find clang)
CC_arm64 := $(shell xcrun --sdk iphoneos --find clang)
CC_armv7 := $(shell xcrun --sdk iphoneos --find clang)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
export GOOS := darwin
export CGO_ENABLED := 1
@@ -28,7 +28,7 @@ version-header: $(DESTDIR)/wireguard-go-version.h
GOBUILDARCH := $(GOARCH_$(shell uname -m))
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
GOBUILDVERSION := 1.11.2
GOBUILDVERSION := 1.11.4
GOBUILDTARBALL := https://dl.google.com/go/go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
export GOROOT := $(BUILDDIR)/goroot
@@ -57,16 +57,15 @@ $(foreach FILE,$(UPSTREAM_FILES),$(eval $(call copy-src-to-build,../wireguard-go
$(foreach FILE,$(DOWNSTREAM_FILES),$(eval $(call copy-src-to-build,src/,$(FILE))))
$(BUILDDIR)/.prepared: $(GOROOT)/bin/go
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CC="$(CC_$(ARCH))" CGO_CFLAGS="$(CGO_FLAGS_$(ARCH))" CGO_LDFLAGS="$(CGO_FLAGS_$(ARCH))" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
chmod -fR +w "$(GOPATH)/pkg/mod"
touch "$@"
define libwg-go-a
$(BUILDDIR)/libwg-go-$(1).a: $(BUILDDIR)/.prepared
cd "$(BUILDDIR)" || exit $$$$?; \
CC="$(CC_$(1))" \
CGO_CFLAGS="$(CGO_FLAGS_$(1))" \
CGO_LDFLAGS="$(CGO_FLAGS_$(1))" \
CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \
GOARCH="$(GOARCH_$(1))" \
go build -tags ios -ldflags=-w -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive && go version > "$(BUILDDIR)/.gobuildversion"; \
ret=$$$$?; \
@@ -80,16 +79,11 @@ $(DESTDIR)/wireguard-go-version.h: ../wireguard-go/version.go
$(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
@mkdir -vp "$(DESTDIR)"
xcrun --sdk iphoneos lipo -create -output "$@" $^
$(DESTDIR)/example: example.c $(DESTDIR)/libwg-go.a
xcrun --sdk iphoneos clang -framework CoreFoundation -isysroot "$(shell xcrun --sdk iphoneos --show-sdk-path)" -arch arm64 -arch armv7 -L"$(DESTDIR)" -lwg-go -o "$@" "$<"
example: $(DESTDIR)/example
$(LIPO) -create -output "$@" $^
clean:
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/example" "$(DESTDIR)/wireguard-go-version.h"
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h"
install: build
.PHONY: clean build example version-header install
.PHONY: clean build version-header install
-46
View File
@@ -1,46 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#include "wireguard.h"
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>
static struct {
int something;
} ctx;
static bool is_closed = false;
ssize_t do_read(void *ctx, unsigned char *buf, size_t len)
{
printf("Reading from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
sleep(1);
return is_closed ? -1 : 0;
}
ssize_t do_write(void *ctx, unsigned char *buf, size_t len)
{
printf("Writing from instance with ctx %p into buffer %p of length %zu\n", ctx, buf, len);
return len;
}
void do_log(int level, const char *msg)
{
printf("Log level %d: %s", level, msg);
}
int main(int argc, char *argv[])
{
int handle;
printf("WireGuard Go Version %s\n", wgVersion());
wgSetLogger(do_log);
handle = wgTurnOn((gostring_t){ .p = "test", .n = 4 }, (gostring_t){ .p = "", .n = 0 }, 0, do_read, do_write, &ctx);
sleep(5);
is_closed = true;
wgTurnOff(handle);
return 0;
}
+70 -95
View File
@@ -25,8 +25,6 @@ import (
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"unsafe"
)
@@ -34,68 +32,25 @@ var loggerFunc unsafe.Pointer
var versionString *C.char
type CLogger struct {
level C.int
interfaceName string
level C.int
}
func (l *CLogger) Write(p []byte) (int, error) {
if uintptr(loggerFunc) == 0 {
return 0, errors.New("No logger initialized")
}
message := C.CString(l.interfaceName + ": " + string(p))
message := C.CString(string(p))
C.callLogger(loggerFunc, l.level, message)
C.free(unsafe.Pointer(message))
return len(p), nil
}
type DeviceState struct {
device *Device
logger *Logger
endpointsTimer *time.Timer
endpointsSettings string
}
var tunnelHandles map[int32]*DeviceState
func listenForRouteChanges() {
//TODO: replace with NWPathMonitor
data := make([]byte, os.Getpagesize())
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
if err != nil {
return
}
for {
n, err := unix.Read(routeSocket, data)
if err != nil {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EINTR {
continue
}
return
}
if n < 4 {
continue
}
for _, deviceState := range tunnelHandles {
if deviceState.endpointsTimer == nil {
deviceState.endpointsTimer = time.AfterFunc(time.Second, func() {
deviceState.endpointsTimer = nil
bufferedSettings := bufio.NewReadWriter(bufio.NewReader(strings.NewReader(deviceState.endpointsSettings)), bufio.NewWriter(ioutil.Discard))
deviceState.logger.Info.Println("Setting endpoints for re-resolution due to network change")
err := ipcSetOperation(deviceState.device, bufferedSettings)
if err != nil {
deviceState.logger.Error.Println(err)
}
})
}
}
}
}
var tunnelHandles map[int32]*Device
func init() {
versionString = C.CString(WireGuardGoVersion)
roamingDisabled = true
tunnelHandles = make(map[int32]*DeviceState)
tunnelHandles = make(map[int32]*Device)
signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2)
go func() {
@@ -111,7 +66,6 @@ func init() {
}
}
}()
go listenForRouteChanges()
}
//export wgSetLogger
@@ -119,44 +73,14 @@ func wgSetLogger(loggerFn uintptr) {
loggerFunc = unsafe.Pointer(loggerFn)
}
func extractEndpointFromSettings(settings string) string {
var b strings.Builder
pubkey := ""
endpoint := ""
listenPort := "listen_port=0"
for _, line := range strings.Split(settings, "\n") {
if strings.HasPrefix(line, "listen_port=") {
listenPort = line
} else if strings.HasPrefix(line, "public_key=") {
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
pubkey = line
} else if strings.HasPrefix(line, "endpoint=") {
endpoint = line
} else if line == "remove=true" {
pubkey = ""
endpoint = ""
}
}
if pubkey != "" && endpoint != "" {
b.WriteString(pubkey + "\n" + endpoint + "\n")
}
return listenPort + "\n" + b.String()
}
//export wgTurnOn
func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
interfaceName := string([]byte(ifnameRef))
func wgTurnOn(settings string, tunFd int32) int32 {
logger := &Logger{
Debug: log.New(&CLogger{level: 0, interfaceName: interfaceName}, "", 0),
Info: log.New(&CLogger{level: 1, interfaceName: interfaceName}, "", 0),
Error: log.New(&CLogger{level: 2, interfaceName: interfaceName}, "", 0),
Debug: log.New(&CLogger{level: 0}, "", 0),
Info: log.New(&CLogger{level: 1}, "", 0),
Error: log.New(&CLogger{level: 2}, "", 0),
}
logger.Debug.Println("Debug log enabled")
tun, _, err := tun.CreateTUNFromFD(int(tunFd))
if err != nil {
logger.Error.Println(err)
@@ -184,27 +108,78 @@ func wgTurnOn(ifnameRef string, settings string, tunFd int32) int32 {
if i == math.MaxInt32 {
return -1
}
tunnelHandles[i] = &DeviceState{
device: device,
logger: logger,
endpointsSettings: extractEndpointFromSettings(settings),
}
tunnelHandles[i] = device
return i
}
//export wgTurnOff
func wgTurnOff(tunnelHandle int32) {
deviceState, ok := tunnelHandles[tunnelHandle]
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return
}
delete(tunnelHandles, tunnelHandle)
t := deviceState.endpointsTimer
if t != nil {
deviceState.endpointsTimer = nil
t.Stop()
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 wgBindInterfaceScope
func wgBindInterfaceScope(tunnelHandle int32, ifscope int32) {
var operr error
device, ok := tunnelHandles[tunnelHandle]
if !ok {
return
}
device.log.Info.Printf("Binding sockets to interface %d\n", ifscope)
bind := device.net.bind.(*NativeBind)
for bind.ipv4 != nil {
fd, err := bind.ipv4.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
}
break
}
for bind.ipv6 != nil {
fd, err := bind.ipv6.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
}
break
}
deviceState.device.Close()
}
//export wgVersion
+3 -1
View File
@@ -12,8 +12,10 @@
typedef struct { const char *p; size_t n; } gostring_t;
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 int wgTurnOn(gostring_t settings, int32_t tun_fd);
extern void wgTurnOff(int handle);
extern int64_t wgSetConfig(int handle, gostring_t settings);
extern void wgBindInterfaceScope(int handle, int32_t ifscope);
extern char *wgVersion();
#endif