Compare commits

...

459 Commits

Author SHA1 Message Date
a5e7c3906b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 21:25:38 +01:00
b21fdfed67 wireguard-go-bridge: do not use getdirentries64 on macos
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 21:23:46 +01:00
5f8843e247 iOS: Delete confirmation prompt should be a question
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-19 21:23:46 +01:00
7a3f65fd2f macOS: Add 'Deactivate' status menu item
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-19 21:23:46 +01:00
dca0fb29f6 Version: CFBundleVersion must always increase for macOS app store
So we'll just start doing it like that, then.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 06:00:40 +01:00
af9c800af8 Swiftlint: variable_name -> identifier_name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 22:26:13 -06:00
a9b925c69b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 22:13:27 -06:00
f93f9d62f4 macos: TunnelsList: set allowsEmptySelection after making initial selection
Otherwise we never get the event that the selection changed, so we don't
wind up showing anything in the details pane.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 22:13:27 -06:00
fc163fc9ff iOS: Consolidate all showConfirmationAlert()s into one place
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 14:54:05 -06:00
adc5a7cac2 iOS: Tunnels list: Ability to remove multiple tunnels at a time
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 14:54:05 -06:00
0dd22ca45a iOS: Tunnel edit: Add missing enum values
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 14:54:05 -06:00
bca9fead5e macOS: ButtonedDetailViewController: Set min dimensions
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-19 01:28:52 +05:30
51822f722a ringlogger: document races
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 12:50:00 -06:00
121d223229 macOS: Tunnels list: Double-click to activate / deactivate
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 12:03:41 +05:30
439fb6bbac macOS: Tunnels list: Don't allow empty selection
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 12:03:41 +05:30
9c8231dcf7 on-demand: macOS: Remove unused class ControlRow
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
0440c4a33a on-demand: macOS: Integrate Ethernet and Wi-Fi controls in one row
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
01be43aa7a on-demand: View model should account for isActivateOnDemandEnabled
This is needed to correctly handle NETunnelProviderManager's
isOnDemandEnabled property getting changed outside of the app.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
e29c6900e5 on-demand: macOS: Disable SSIDs field when adding a tunnel
It shouldn't be editable when the VPN prompt is shown.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
a334c25aff on-demand: iOS: Disable selection in SSID detail table view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
f56b2ad968 on-demand: macOS: Remove unused class PopupRow
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
503ac6c8a2 on-demand: macOS: Auto-complete SSIDs based on currently connected SSID
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
5f30e021ef on-demand: iOS: Change wording for add-SSIDs rows
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
d748382fce on-demand: "Only selected SSIDs" -> "Only these SSIDs"
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
63299a2752 on-demand: macOS: Tunnel detail: List SSIDs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
b7f8f74b56 on-demand: iOS: Only n SSIDs / Except m SSIDs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
8e5a9215de on-demand: iOS: Show list of SSIDs in a separate screen
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
64925cab89 on-demand: iOS: SSIDs view: Always show the selected SSIDs section
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
062b4d4b16 on-demand: Remove ActivateOnDemandSetting type
The ActivateOnDemandOption type shall be used instead

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
d9bdc61fb9 on-demand: TunnelViewModel: Remove unused on-demand-related methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
0ae8d25134 on-demand: macOS: Tunnel detail: Show SSID info
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
574d8433b3 on-demand: iOS: Update on-demand info shown in tunnel edit view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
bd339e2876 on-demand: ActivateOnDemandViewModel: Uniquify SSIDs list
And if SSIDs list is empty, fall back to .anySSID option

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
fff75adfe1 on-demand: macOS: Support SSIDs in on demand activation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
01604dd8d1 on-demand: iOS: Tunnel detail: Show SSID info
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
bdeb89a9e5 on-demand: iOS: Add ability to add current SSID
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
36dc252512 on-demand: iOS: Xcode: Add ability to access current SSID
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
5941bf181c on-demand: iOS: Support for SSIDs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
7a450089c0 on-demand: Introducing ActivateOnDemandViewModel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
5d757982ba on-demand: Infrastructure for supporting SSID-based rules
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
3767a12983 on-demand: Simplify OS-specific code for interface type selection
Previously, the enum values themselves were different for iOS and macOS.
With this commit, the enum values are common, and only how they're handled
is specific to iOS and macOS.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
9795b0609a macOS: Localize tooltips
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
0f98312d15 macOS: Tunnel detail: Make the Activate button part of the list view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
f81275812c macOS: Nullify observationToken on prepareForReuse()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
b2b5e0e379 TunnelName: sort correctly with numbers and capitals
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 06:46:55 +01:00
a6f80135ef ringlogger: support mpsc for singlefile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-17 08:51:27 +01:00
e23c221aff macOS: Tunnel detail: Activate / Deactivate is now a button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-17 02:27:46 +05:30
50bc994762 macOS: Tunnel detail: Show the status in the list view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-17 02:27:46 +05:30
3e05da4486 macOS: KeyValueImageRow class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-17 02:27:46 +05:30
c750f28c67 wireguard-go-bridge: update deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-12 10:45:14 +01:00
f6c70500a7 wg-quick parser: trim \r as well
The influx of Windows users has already begun to infect our nice
project.

Reported-by: Cosku Bas <cosku.bas@gmail.com>
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-11 14:05:16 -06:00
663923864c TunnelsManager: Don't restart if only on-demand setting has changed
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-11 13:20:21 +05:30
9250780ffc macOS: Ability to remove multiple tunnels at a time
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-10 20:02:19 +05:30
9bc17034dd TunnelsManager: Support for removing multiple tunnels at a time
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-10 19:43:27 +05:30
db6f0729c6 macOS: Generalize NoTunnelsDetailVC into a ButtonedDetailVC
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-10 19:22:33 +05:30
4503c11b0c wireguard-go-bridge: use system go installation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-08 05:56:00 +01:00
fe4f8b666d Importing: Only the main thread shall access lastFileImportErrorText
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-05 16:11:57 +05:30
90c0f7e92e Importing: Make use of lastError returned from TunnelsManager.addMultiple()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-05 16:11:41 +05:30
3afcee04be TunnelsManager: addMultiple() should also return the last error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-05 15:29:28 +05:30
202e7a4890 Importing: Simplify TunnelImporter
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-04 14:13:49 +05:30
d7b16ffb1f wireguard-go-bridge: use go modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-03 06:28:07 +01:00
b1dabf5a00 wireguard-go-bridge: update to Go 1.12
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-27 06:24:56 +01:00
a389bd93cb Importing: macOS: Support importing of multiple files at a time
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-25 18:43:20 +05:30
b2a2110d8c Importing: Use case-insensitive comparison for zip extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-25 16:21:29 +05:30
5ed28907ec iOS: Hack to restart active tunnel after adding a new tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-24 19:30:14 +05:30
ab6d714070 Importing: Show OS error when unable to open a .conf file
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-24 16:50:57 +05:30
d3df8734c2 macOS: Tunnel edit: Disable user interaction when OS VPN prompt is shown
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-24 16:06:37 +05:30
ea5996abe0 macOS: Tunnel edit: s/populateTextFields()/populateFields()/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-24 15:14:35 +05:30
ce405f856e macOS: When programmatically selecting a tunnel, also scroll if required
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-22 18:18:53 +05:30
98a967acc8 macOS: Replace NSSegmentedControl with NSPopUpButton and NSButton
Thereby avoiding the hacky way of showing the menus.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-22 17:59:41 +05:30
b01d09dfb5 Importing: Give a clearer error message on importing an invalid config
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-22 13:03:53 +05:30
7a580e8941 macOS: Show 'quitting with active tunnel' only when appropriate
Not when logging off or when the machine's shutting down

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-22 13:03:53 +05:30
39fb52a2e3 macOS: Fix removal of DNSes from AllowedIPs when DNS has changed
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-21 18:17:28 +05:30
69a064d954 iOS: On changing DNS, update AllowedIPs with the current DNS servers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-21 17:57:13 +05:30
eb684ef711 macOS: On saving, update AllowedIPs with the current DNS servers
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-21 17:57:13 +05:30
b0eff424f9 Importing: Better error message when .conf file is not readable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-21 17:57:13 +05:30
c195760b15 macOS: Specify crypto compliance
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-19 16:12:33 +01:00
ba3f0db92c TunnelViewModel: Remove DNS from AllowedIPs when unchecking 'Exclude private IPs'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 19:57:31 +05:30
5031a7db4c macOS: Exclude private IPs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 18:25:17 +05:30
a355232e09 TunnelViewModel: Minor refactoring of exclude private IPs handling
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 17:35:33 +05:30
6f7214ff38 ConfTextStorage: lowercase only once
Also fix submodule regression.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-16 17:18:10 +05:30
4c88f477a2 ConfTextStorage: Let's keep the AllowedIPs and DNS servers as strings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 01:20:11 +05:30
2fb9d6af71 ConfTextStorage: Make fieldType an enum
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 00:26:49 +05:30
38ac66071c ConfTextStorage: keep track of single peer state for exclude private IPs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 19:44:06 +01:00
910fdfc321 macOS: Tunnel detail: Set min width/height
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-15 15:44:11 +05:30
c38a88988b macOS: Tunnels list: Use constant width for the table view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-15 15:40:12 +05:30
cf51344444 .mobileconfig: fix lists
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-15 14:05:15 +05:30
fda36b571d README: supports macOS
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:39:34 +01:00
1be3224d7a README: recursive cloning
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:37:02 +01:00
ca088e9ddc README: Xcode has a lowercase 'c'
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:35:10 +01:00
fcca2d4fec macOS: Show privacy notice on adding first tunnel
App store reviewers don't understand that this isn't a service.

Revert this as soon as they come to their senses.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 01:14:14 +01:00
58181a4d40 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 23:07:06 +01:00
cf816ede13 wireguard-go: bump for ARM64 ChaCha20
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 12:00:59 +01:00
fbac282bdc .mobileconfig: fix formatting
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:31:02 +01:00
61f5e017c8 .mobileconfig: note keychain limitation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:04:54 +01:00
4547e01283 Preshared key field in the detail view should just say 'enabled'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:30:59 +05:30
5792db22a6 Log migration of tunnel configuration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:17:32 +05:30
9d5aa1d8fa Document installing WireGuard tunnels using Configuration Profiles
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:14:53 +05:30
6331b81b5d Migrate when we notice a new tunnel in reload()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 17:43:40 +05:30
77f929789c Don't migrate in asTunnelConfiguration()
It causes problems when installing a tunnel through a
Configuration Profile on macOS and activating it first through
Network Preferences.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 17:37:27 +05:30
b5b72b309f Info.plist: Localize with InfoPlist.strings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-11 16:08:55 +05:30
966fa7909b macOS: Change keyboard shortcut for importing to Cmd+O
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 17:55:50 +05:30
115059f2bb macOS: Adapt to the new applyConfiguration API
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 03:35:24 +05:30
e53c2d4d17 iOS: Rewrite applying runtime configuration
To make scrolling smoother while the fields are modified

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 03:35:24 +05:30
0a3a5ee900 Importing: Ignore case in matching file extensions inside zip files
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-09 19:42:50 +05:30
7720307fc9 TunnelsManager: No need to access tunnelConfiguration on status change
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-09 19:42:50 +05:30
ea827e2ebd Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 17:32:38 +01:00
91b1734b7a Fix writing of preshared key to config format
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-09 14:57:24 +05:30
bac4851e95 Project: don't embed swift binaries into appex
Otherwise we're rejected from the app store.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 17:26:05 +01:00
0e2556544e Global: fix swiftlint issues
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 17:07:28 +01:00
2ec51ba8cf wireguard-go-bridge: get rid of nopie warning
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 16:42:25 +01:00
998bbd73e4 wireguard-go-bridge: Cache go tarballs
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 16:23:40 +01:00
38a6ba7091 KeyEncoding: rename file to match extension filename style
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 16:15:10 +01:00
407b367c8d Key: we already do len checking in C
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 14:39:59 +01:00
a231410c52 Info.plist: Add missing key types
I worry that LSMinimumSystemVersion in the extension's plist might be
problematic, since that same plist runs on macOS and iOS. We _might_
need to bifurcate.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 03:47:36 +01:00
f518c00722 Version bump
First Mac App Store release if all goes well.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 03:28:28 +01:00
0539929d0c Key: Use C implementation instead
Swift compiles so slowly and it's unclear all of the insane type punning
was even correct.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 03:23:15 +01:00
05547861b6 Key: Constant time encoding
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-08 03:23:15 +01:00
9eed5fd898 TunnelsManager: Ignore status changes on tunnel providers we don't have
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 04:40:11 +05:30
1b8b9ed7ee iOS: Use shorter pretty time
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 04:40:11 +05:30
ef6af03412 iOS: Tunnel detail: Turn off animation when showing fields changing
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 04:40:11 +05:30
a99a755c34 macOS: Show alert if exiting with an active tunnel
Instead of deactivating the tunnel.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 04:40:11 +05:30
ecd66defe5 TunnelsManager: Don't lose .restarting state
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 02:34:30 +05:30
1f3ec042e0 TunnelsManager: Log startDeactivation calls
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-08 02:34:29 +05:30
631e9bb70d wireguard-go: Bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-07 21:09:53 +01:00
446c3e3698 Enable hardened runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-07 19:17:27 +01:00
02e9172940 NetworkExtensionMac: Don't forget to link to the networkextension framework
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-07 19:13:43 +01:00
8676f3a663 StatusItemController: Show animation when deactivating
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-07 18:19:15 +01:00
394a0cbeb0 PacketTunnelProvider: proper fix for 32073323
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-07 15:01:37 +01:00
868fee0477 TunnelsManager: When creating/modifying a tunnel, update the associated object
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-07 18:18:04 +05:30
0cddb562fc macOS: prohibit multiple instances of app
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 06:20:23 +01:00
bebcaa012b PrivateDataConfirmation: prompt with touch/face/pin/password ID for viewing/exporting keys
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 06:20:23 +01:00
ed8dc516dc LegacyConfig: Remove and support plaintext for .mobileconfig
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 06:20:23 +01:00
8c3557a907 Keychain: store configurations in keychain instead of providerConfig
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 06:20:23 +01:00
a26d620f11 TunnelsManager: cache access to configuration object
Supposedly we never change it once per object, so we do the objective C
hack of adding it cached to the extension. This prevents 1000s of calls
to the keychain and improves the speed of imports.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 01:52:31 +01:00
30a73a75fd Project: Remove OS name from appex file name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 01:52:31 +01:00
71d26b4122 TunnelsManager: Wait for 6 seconds on deactivation instead of 5
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-06 01:52:31 +01:00
71525c9d4e wg-quick conf parser: Handle inline comments correctly
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 16:45:32 +05:30
02a96d4566 macOS: Select tunnel after adding it with 'Add empty tunnel'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 16:39:19 +05:30
466db151b8 macOS: Ensure fields are updated on saving
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 16:15:43 +05:30
1be133f269 iOS: Ensure fields are updated on saving
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 15:54:23 +05:30
80de2ac6ac macOS: Apply runtime configuration by diff-ing
And apply the diff on the tableView as insertRows/removeRows.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 12:36:35 +05:30
8a6a60482c TunnelViewModel: Don't call peer change handler if there are no changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 12:36:34 +05:30
657ec34d19 macOS: Tunnel detail: Refactor calculation of tableViewModelRows
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-05 12:36:34 +05:30
f7a31ca7bb x25519: demand RNG is successful
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-05 00:49:48 +01:00
3c61db3a21 Config: Add template for macOS key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-03 13:47:42 +01:00
618d89941a iOS: SwitchCell should hold the observation token
And should nil the token when preparing for reuse.

This also reverts "iOS: Tunnel detail: Refactor updation of status"

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-03 12:40:19 +05:30
cbc602245e iOS: KeyValueCell should hold the observation token
And should nil the token when preparing for reuse.

Otherwise, the observation closure is still active even after the cell
gets reused.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-03 12:40:19 +05:30
ca61e09536 wireguard-go: bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-03 00:20:24 +01:00
4ff6105053 iOS: Apply runtime configuration by diff-ing
And apply the diff on the tableView as insert/remove/reloads.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-02 19:22:01 +05:30
4134baced1 iOS: Tunnel detail: Keep track of visible fields with a [Bool] array
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-02 18:10:57 +05:30
0c5739db82 Strings: fix backwards clock wording
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-01 14:49:38 +01:00
1f51ff6b17 iOS: Tunnel detail: Reload runtime config every second
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-01 15:27:17 +05:30
08e5d65045 iOS: Tunnel detail: Refactor updation of status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-31 18:46:46 +05:30
1189b3d700 Fix handling of 'PersistentKeepalive: every n seconds'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-31 18:22:08 +05:30
f292a0ec7a iOS: Make it compile again
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-31 18:17:04 +05:30
3b29578524 Configure timers to fire even when tracking mouse events
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-31 17:04:34 +05:30
70ac48ceba macOS: Tunnel detail: Reload runtime config every second
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-31 16:48:51 +05:30
acecc70397 Logger: Convert do-catch to try?
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-28 17:50:48 +05:30
b0bb2e993a Runtime info: Make bytecount and timestamp info prettier
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-28 17:27:08 +05:30
d1f83d167e Persistent Keepalive detail should read 'every n seconds'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-28 16:07:28 +05:30
a796c6c485 TunnelsManager: Invoke reload() in a subsequent runloop
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-26 14:31:42 +05:30
6ad3487a9d macOS: Delay .deactivated status to workaround system bug
For some time after it's connection status becomes .disconnected,
if a tunnel gets started, it gets automatically killed by the system
after ~25 seconds.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-26 14:25:38 +05:30
eabeb8ff05 macOS: Select the active tunnel when showing the manage tunnels window
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-24 18:35:11 +05:30
52eec55d36 TunnelsTracker: Simplify using TunnelsManager.tunnelInOperation()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-24 18:11:55 +05:30
3c80490273 TunnelsManager: func tunnelInOperation()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-24 18:11:26 +05:30
c36a9e4ffd macOS: Ensure status is up-to-date on startup
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-24 16:51:01 +05:30
812e660491 Config file parsing: Fix bug when there are comments at the end
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-24 16:23:07 +05:30
2fe9f83ba5 macOS: show runtime configuration in tunnel manager
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-24 01:37:57 +01:00
22625e8cc4 Tunnel: support getting runtime configuration
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-24 01:37:57 +01:00
ab3cbee6a2 wireguard-go-bridge: allow querying internal settings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-24 01:22:04 +01:00
fadef52e1b wireguard-go-bridge: fix standalone build
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-23 21:59:51 +01:00
19f353127e macOS: Tunnel detail: Fix updation of tunnelEditVC
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-23 19:26:43 +05:30
e5a76be6fd macOS: Deactivate any active tunnel when app exits
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-23 16:31:30 +05:30
76527ef0f8 macOS: Adapt to TunnelsManagerListDelegate changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-23 16:21:20 +05:30
11e44f9ae5 iOS: Fix stale tunnel being shown on iPad
When the detail view is shown in the iPad and we delete
the current tunnel with a list view swipe rather than the delete button,
the detail view should go blank.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-23 16:11:55 +05:30
77d4a02139 iOS: Fix handling of deletion outside app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-23 14:48:45 +05:30
54f45cb3f8 macOS: reload: Iterate in reverse
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 20:06:52 +05:30
704de3b26c TunnelsManager: refresh status after replacing insides
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-22 15:35:14 +01:00
d05b735703 TunnelsManager: use new helper
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-22 15:20:57 +01:00
9f362e8cb0 macOS: Tunnel edit: Handle deletion outside app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 19:30:21 +05:30
2677efc9bf macOS: Tunnel detail: Handle deletion outside app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 19:30:21 +05:30
2aad8cf0e8 macOS: Handle tunnel deletions outside the app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 19:30:21 +05:30
668c4a475c macOS: remove mobile network tweeks
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-22 13:11:28 +01:00
557b093232 MacOS: StatusMenu: Properly localize menu title
Partially revert "macOS: StatusMenu: Remove unused menu title"

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-22 11:26:07 +01:00
6f9fa35c5a macOS: Disable save button if the syntax highlighter detects any errors
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 12:57:32 +05:30
d37e230ee0 macOS: Fix crash when importing using NoTunnelsDetailVC's button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 05:00:39 +05:30
e812683d6c macOS: StatusMenu: Remove unused menu title
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:41:52 +05:30
6d8e5cf3ed Let there be newlines at the end of all files
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:40:47 +05:30
244a2df2dd Fix localization
- Use Unicode ellipses
- Use single quotes everywhere
- Use smart quotes
- Minor text change ("You cannot undo this action.")

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:35:13 +05:30
0848765f50 macOS: Use Unicode version of '...' for menu text
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:16:22 +05:30
8951ea338f macOS: Fix status-related menu items
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:11:24 +05:30
273ee04450 Better os() directives
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 04:06:37 +05:30
e2b068af1a macOS: Tunnel edit: actually clean up error handling
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-21 23:21:47 +01:00
4e2f4e7124 XCode: set default signing identity back
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-21 23:17:08 +01:00
6509009afc macOS: Tunnel edit: Clean up error handling when saving
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 03:26:01 +05:30
0b2a4c2811 macOS: Observe private key changes for new tunnels too
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-21 22:55:25 +01:00
e1198b6e08 macOS: Better highlighter default value and move c implementation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-21 22:22:20 +01:00
d7a88300f6 macOS: Make highlighter themes static
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-21 22:13:14 +01:00
b22aeaeb20 Avoid using return in single-line closures
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 02:01:32 +05:30
e0798b8a97 macOS: Make color theme use a dict
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 02:01:32 +05:30
1bc2177883 macOS: Reset attributes for each syntax highlight cycle
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 01:19:16 +05:30
15517c4c3d macOS: Refactor syntax highlighting
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-22 01:18:07 +05:30
f6c9ce2d71 macOS: Simplify NSColor extension
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-21 19:07:42 +05:30
5aa0f1a25f macOS: show icon for inactive state
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-18 21:17:53 +01:00
602639ee25 highlighter: do not rely on localized case comparisons
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-18 09:32:49 +01:00
d06887d435 Xcode: move directives to toplevel project when possible
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-18 09:32:49 +01:00
470e4e7f7f global: Fix up copyright headers
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-18 09:32:49 +01:00
86165d25f7 TunnelsManager: Remove unused variable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-18 09:32:49 +01:00
69b973efdc macOS: Tunnel detail: Better alignment for bottom controls
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-18 09:32:49 +01:00
dc8f27c5c3 macOS: Rafactor by introducing a TunnelsTracker
The TunnelTracker is now the central place to track what the current
tunnel is, and for keeping track of the tunnel list.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-18 01:34:24 +05:30
796342ddec macOS: Fix autolayout errors on Add Empty Tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 14:20:09 +05:30
b7a5b8eff1 macOS: Update copyright year
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 14:08:11 +05:30
8a0245190c macOS: Make sure app is active when showing the About dialog
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 14:05:23 +05:30
98775bf8a0 macOS: Application: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 14:03:59 +05:30
7d5eb476e4 macOS: Manage tunnels: Make keyboard shortcuts discoverable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 02:43:24 +05:30
c477d24d67 macOS: Manage tunnels: Keyboard shortcuts
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 01:44:50 +05:30
bf9cb092a9 macOS: Tunnel edit: Rename action handling methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 01:28:57 +05:30
dbd5108475 macOS: Tunnel detail: Rename action handling methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 00:41:32 +05:30
47c5f23a0a macOS: Tunnels list: Rename action handling methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-17 00:38:44 +05:30
a2871e63a7 macOS: Support window management keyboard shortcuts
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-16 18:01:59 +05:30
811714e21a macOS: Networks should show allowedIPs and disappear when inactive
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-15 22:35:30 +01:00
f63c9fd598 macOS: Use tunnelOverheadBytes for automatic MTU in macOS
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-16 01:51:56 +05:30
e29cf19fdd macOS: Different status bar icon looks for different states
- Looks dimmed when no tunnel is active
- Looks normal when a tunnel is active
- Animates when a tunnel is activating

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-16 01:26:10 +05:30
26ea353933 macOS: Add About dialog
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:37 +05:30
cd5427ef92 macOS: Add app icon
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:37 +05:30
595f7943e2 macOS: Edit view: Auto hide editor scrollbars
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:37 +05:30
55f022688d macOS: To set default size, change frame instead of min size
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:37 +05:30
96bd50504b macOS: Fix editor scrolling
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:37 +05:30
97fec0d992 Default view controller sizes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2019-01-14 14:52:36 +05:30
470532d146 ConfTextView: enable undo and disable junk
Double space stil makes a period, unfortunately.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-14 14:52:36 +05:30
321b88864c Cut/copy/paste now work
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2019-01-14 14:52:36 +05:30
ba731e0099 Resync highlighter
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-14 14:52:36 +05:30
70848c04de Syntax highlighter color updates
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2019-01-14 14:52:36 +05:30
13e8c6b178 macOS: Support for on-demand activation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:36 +05:30
53e915c578 macOS: Quit menu item
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:36 +05:30
bc8ea55023 macOS: Get the app back in focus after macOS' VPN prompt
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
e0af06844d macOS: Fix 'Network' entry in menu
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
8980b5a524 macOS: Ensure a tunnel is selected when '-' is clicked
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
df8ab96139 macOS: Handle errors from TunnelsManager.create()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
4a8366421f iOS: Export log: Should present error from the main thread
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
c9ee549a2e macOS: Localize export sheets
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
f5059ce55b macOS: Import sheet button should say 'Import'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
5a73244ec9 macOS: Tunnel detail: Ensure long keys fit
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
922b6f76b2 macOS: Manage tunnels: Add empty tunnel pulldown menu implementation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
fc9e2de72c macOS: Update detail view after editing
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
80977b95de macOS: Edit view: Update public key as you edit
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:35 +05:30
bbeb732ef3 Highlighter: Report each key type separately
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:34 +05:30
94c4922913 Parsing: Always error on unrecognized keys
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:34 +05:30
fc03c635c1 Parsing: Error on duplicate entries
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:34 +05:30
b0612df990 macOS: Edit view: Validate and save
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:34 +05:30
c2a6241b5c macOS: Refactor config file parsing
- To report more fine grained errors
- To make the parse errors conform to WireGuardAppError

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:34 +05:30
96fa6d3ba6 Syntax highlighter color updates
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2019-01-14 14:52:34 +05:30
64fe415879 Highlighter: use original file from contrib/examples/highlighter
This makes it easier to track updates and make diffs. Also, disable
things we don't support in the NetworkExtension app.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-14 14:52:34 +05:30
59bfa7f1df Added syntax highlighting conf textview
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2019-01-14 14:52:34 +05:30
c2633987c3 macOS: Tunnel edit view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
f7b2f73015 macOS: Rename *Cell to *Row
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
c72f7056b3 macOS: On adding the first tunnel, select it
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
cb778fe7e0 macOS: Consolidate presenting of the import panel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
f3c2904241 macOS: Manage tunnels: Handle the case when there are no tunnels
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
df8b400850 macOS: Present tunnel activation errors from the window when possible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:33 +05:30
252d940d34 macOS: Present errors as a sheet when applicable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
efb64b1959 macOS: Manage tunnels: Remove tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
dfc4b37518 macOS: Manage tunnels: Update tunnels list on changes
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
60cfceec4f macOS: Manage tunnels: Export log pulldown menu implementation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
361830a69e macOS: Manage tunnels: Export tunnels pulldown menu implementation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
f6ea25573b macOS: Xcode: Add ablity to save files
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
de12c27d5b macOS: Manage tunnels: Select first tunnel on showing the window
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
a221cb566b macOS: Manage tunnels: Set window title
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
f33cd0b6fd macOS: Manage tunnels: Import pulldown menu implementation
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
38bb0faf86 macOS: Manage tunnels: Localize pulldown menu items
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
8d9c5e2950 macOS: Show open panel as sheet on manage window
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
09f4be17de macOS: Manage tunnels: Adjust spacings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:32 +05:30
60e18dfdd5 macOS: Manage tunnels: Add a box around the detail view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
5bc0c5b2b4 macOS: Manage tunnels: Show status checkbox and edit button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
4a4eeb4a21 macOS: s/macMenuStatus/macStatus/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
ada7db3dca macOS: Manage tunnels: Tunnel detail view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
c946c0ea48 macOS: Manage tunnels: Add a filler button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
4a4690b5fa macOS: Manage tunnels: Fix list view look
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:31 +05:30
37fce31d16 macOS: Manage tunnels: Add buttons to the bottom of the list view
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
7934d6b0c7 macOS: Manage tunnels window: Tunnels list
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
98e9088aba macOS: Capitalize All Rights Reserved
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
2c81c3a379 macOS: Show status as disabled menu items
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
04f6ee0f11 macOS: Ability to activate / deactivate a tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
545f8c88f4 macOS: Ability to import tunnels from file
For now, the open panel shows as a separate window.
Later, we'll open it as a sheet on the 'Manage tunnels' window.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:30 +05:30
6a27626fc0 iOS: Refactor importFromFile
So that it can be used in macOS as well

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
fb1607d4a2 macOS: Add tunnel management menu items
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
51a2c272b9 macOS: Specify app is an 'agent'
This hides the app from the Dock, while still enabling the app
to come to the foreground if required.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
b5751b6321 macOS: Create status bar with tunnel names
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
110012dbcc macOS: Add status bar icon
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
5c7a149167 macOS: Remove MainMenu.xib
When there's no xib, we should explicitly set the app delegate, so we
override NSApplication and set the app delegate in NSApplication.shared

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:29 +05:30
629009d3be macOS: NE: Add entitlements for making network connections
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
d7d4355f5e Make app groups work on both iOS and macOS
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
55d6961a2f macOS: Add Network Extensions capability to app
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
c8cd663a05 iOS: Fix WireGuardNetworkExtensioniOS target
- Rename WireGuardNetworkExtension.entitlements to WireGuardNetworkExtension_iOS.entitlements

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
a754c4d7ab iOS: Fix WireGuardiOS target
- Move Info.plist and entitlements to WireGuard/UI/iOS/

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
95415cd917 macOS: Fix WireGuardmacOS target
- Include non-UI code from iOS while building
- Add run scripts
- Move files to WireGuard/UI/macOS
- Set Swift-Obj-C bridging header

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:28 +05:30
b32b897181 macOS: Fix WireGuardNetworkExtensionmacOS target
- Build using common network extension code
- Add run scripts
- Set Info.plist to common network extension's Info.plist
- Move entitlements to common network extension folder
- Remove Xcode-generated macOS network extension code
- Set Swift-Obj-C bridging header

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
d5c1acb57e macOS: WireGuardNetworkExtensionmacOS depends on WireGuardGoBridgemacOS
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
573f9640de macOS: Add WireGuardNetworkExtensionmacOS target
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
f6772dc353 macOS: Add WireGuardmacOS target
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
0cbe66df99 Xcode: Add WireGuardGoBridgemacOS target
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
3cd33ebe8f Move iOS images and storyboard into UI/iOS/ folder
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:27 +05:30
c7a40d3cb0 Xcode: Rename iOS targets to include an 'iOS' suffix
But keep the PRODUCT_NAME as 'WireGuard', not 'WireGuardiOS'.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:26 +05:30
d02b0fd10e xcconfig: Make app id platform-specific
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:26 +05:30
09d7a5229a On-Demand: Add support for macOS-specific values
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:26 +05:30
1ca5407b85 wireguard-go-bridge: Make CFLAGS_PREFIX work for macOS as well
For macOS, Xcode doesn't set DEPLOYMENT_TARGET_CLANG_FLAG_PREFIX,
but does set DEPLOYMENT_TARGET_CLANG_FLAG_NAME.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:26 +05:30
10982a57ef import Foundation instead of UIKit wherever possible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-01-14 14:52:26 +05:30
5f15b664fc Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-08 02:13:44 +01:00
49f287439e PacketTunnelSettingsGenerator: use 127.0.0.1 as dummy address
It turns out that using 0.0.0.0 somehow conflicts with DNS lookups when
CLAT is in use.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-08 01:51:12 +01:00
150cd119c7 Avoid dynamic MTU calculations for now
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:23:39 -05:00
e2384e143c Update copyright
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-01-07 19:23:39 -05:00
52c59704de Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-28 19:43:11 +01:00
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
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
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
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
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
035055ef0a SwitchCell nits
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 15:32:29 -06:00
508ba44576 Fix typo for simulator builds
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-22 14:32:39 -06:00
999b761ed0 Remove more comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-23 00:31:44 +05:30
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
aaee3c5cbe Bump to latest wireguard-go release
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 17:38:25 +01:00
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
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
e6e1795d08 TunnelErrors: Add alert text for PacketTunnelProviderError
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 16:05:43 +05:30
fd29cf3402 TunnelStatus: Absorb NEVPNStatus+CustomStringConvertible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-22 15:46:28 +05:30
56ad5f74e9 Also refresh status
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 07:07:53 +01:00
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
0bec5b04b0 All models now Equatable
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 22:57:17 -06:00
d36e7e27ff Clean up trailing whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 05:30:35 +01:00
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
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
8365adf435 Localize remaining strings in network extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 03:42:01 +01:00
9d9859248e RTL support
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 20:37:22 -06:00
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
f2000aa1da Combine double log invocations
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 02:21:07 +01:00
82de3e3090 Bump go bridge
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:58:02 +01:00
41a4c6362a Attempt to strongly recommend things
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:55:42 +01:00
aede9f6e45 Move model helpers to model directory
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 01:36:42 +01:00
1eeed89174 Fixes mock tunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 18:35:01 -06:00
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
4ed646973e Move name from interface to tunnel
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-22 00:28:18 +01:00
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
7b9d4cb9e3 Nuke trailing spaces
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 23:34:56 +01:00
1fecd8eb6c providerConfiguration is now a WgQuickConfig
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:32:08 -06:00
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
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
78b38a4eba Simplify versioning of stored data
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:58:06 +01:00
ec031b1f19 Get rid of superflous isActivateOnDemandEnabled key
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 18:50:32 +01:00
8553723e04 Updated NETunnelProvider save format
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-21 16:42:16 +01:00
38445114e0 NE: simplify logic
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-21 15:56:03 +01:00
a21c569e9f NE: Simplify DNS resolution
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 19:24:22 +05:30
0552d75aa1 Localize all the things
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-21 18:34:09 +05:30
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
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
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
c2131cb757 Added missing param in MockTunnels
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 13:51:44 -06:00
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
a89ad95901 Enabled more swiftlint rules
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 11:22:37 -06:00
5618c465a2 Added a String->[String] helper
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-20 10:46:26 -06:00
de08978a80 TunnelErrors: Remove unused error
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-20 19:44:57 +05:30
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
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
35450bf407 Remove non-helpful comments
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
f93c9797ea Tunnel edit: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 18:35:53 +05:30
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
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
04a8c2ff5a NE: No need for two startTunnel() methods
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-19 13:10:42 +05:30
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
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
3ae9fb538d s/Observervation/Observation/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-18 23:00:27 +05:30
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
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
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
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
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
fc452753a7 Potential fix for insertRowAtIndexPath crash
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-16 21:11:33 -06:00
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
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
a80cf6a0dc Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:55:04 +01:00
727992f5d2 Improve mock tunnels generation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-16 01:51:14 +01:00
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
b3f5635f4e Nuke duplicate file
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:21:49 +01:00
946524aa8b Bump the go runtime
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 06:14:05 +01:00
1450538846 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 05:08:31 +01:00
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
1e9c806614 Fix confusing indentation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 04:42:46 +01:00
ccd8cfe478 KeyValueCells now share code
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 20:02:37 -06:00
cb051f695d Reorganized project structure
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:27:11 -06:00
7a24f18eb7 Most similar views now shared between ViewControllers
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-14 17:15:22 -06:00
83c95dc26d Prettier log time format
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-15 00:08:54 +01:00
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
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
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
73be704b01 Deduplicate functions
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-14 00:01:50 +01:00
2699c613bd Simplify filemanager extension
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:25:18 +01:00
74e983ea6f Can't -> cannot
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 23:17:05 +01:00
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
501e412b84 TunnelsManager: startActivation() need not take a tunnelConfiguration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-14 00:40:18 +05:30
77a26e4cd2 Localize swiftlint
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 20:06:37 +01:00
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
7323a00612 Avoid escaping heap allocation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:43:12 +01:00
a6912ca7a2 Tidy up str to gostr conversion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:36:51 +01:00
b256acc372 TunnelsManager: Remove mentions of 'internal error'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:53:17 +05:30
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
740ffd68b6 Remove unused code: InternetReachability
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:45:21 +05:30
f67e1d8fc4 TunnelsManager: Remove unused variable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:44:13 +05:30
33af8845b6 TunnelsManager: Remove assert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 23:43:15 +05:30
154774ada2 Simplify C strings
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 19:08:38 +01:00
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
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
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
38accad27d More reliable logo sizing
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-13 09:30:13 -06:00
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
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
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
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
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
12503ae51d Logging: ringlogger.c: Trim trailing newlines
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 17:37:14 +05:30
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
5ae9eec555 Avoid using 'VPN' in code where possible
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:20:10 +05:30
6528a581de mv WireGuard/WireGuard/VPN/ WireGuard/WireGuard/Tunnel/
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-13 12:14:21 +05:30
e11224f394 Commit untested ringlogger code
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 06:14:24 +01:00
5971c197bd Remove useless whitespace
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:26:04 +01:00
ecbab37e0e Settings: better padding calculation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-13 05:22:13 +01:00
4eec53d6d3 Fixed hacky logo display for settings
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:16:28 -06:00
8a916beb38 More formatting nits and cyclomatic complexity fixes
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
2018-12-12 21:09:52 -06:00
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
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
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
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
7ef12d93a7 ErrorPresenter: Support onPresented for showErrorAlert(title:,message:)
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-12 19:17:06 +05:30
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
034a1a12f7 Supply missing pieces of path change
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2018-12-12 01:11:43 +01:00
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
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
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
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
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
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
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
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
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
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
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
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
663bb02c68 TunnelsManager: Debugging helpers for tunnel status
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:31:36 +05:30
b491b9c371 TunnelsManager: Handle deactivation of a waiting tunnel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 16:28:41 +05:30
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
aa41bd7d6c Settings: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
ec5be76d06 Tunnel edit: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
692b0c6519 Tunnel detail: Dynamic Type support
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-10 01:45:34 +05:30
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
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
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
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
6838dc3a74 TunnelsManager: Remove unused variables
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 15:00:26 +05:30
46f4be6087 Zip: Fix comment
Signed-off-by: Roopesh Chander <roop@roopc.net>
2018-12-08 13:43:00 +05:30
194 changed files with 12323 additions and 4564 deletions

7
.gitignore vendored
View File

@ -19,6 +19,7 @@ DerivedData
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xcscheme
## Other
*.xccheckout
@ -38,3 +39,9 @@ fastlane/screenshots
fastlane/test_output
Preview.html
output
# Wireguard specific
WireGuard/WireGuard/Config/Developer.xcconfig
# Vim
.*.sw*

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "wireguard-go"]
path = wireguard-go
url = https://git.zx2c4.com/wireguard-go

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

View File

@ -1,4 +1,4 @@
Copyright © 2018 WireGuard LLC. All Rights Reserved.
Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

140
MOBILECONFIG.md Normal file
View File

@ -0,0 +1,140 @@
# Installing WireGuard tunnels using Configuration Profiles
WireGuard configurations can be installed using Configuration Profiles
through .mobileconfig files.
### Top-level payload entries
A .mobileconfig file is a plist file in XML format. The top-level XML item is a top-level payload dictionary (dict). This payload dictionary should contain the following keys:
- `PayloadDisplayName` (string): The name of the configuration profile, visible when installing the profile
- `PayloadType` (string): Should be `Configuration`
- `PayloadVersion` (integer): Should be `1`
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the profile file.
If you install another .mobileconfig file with the same identifier, the new one
overwrites the old one.
- `PayloadUUID` (string): A randomly generated UUID for this payload
- `PayloadContent` (array): Should contain an array of payload dictionaries.
Each of these payload dictionaries can represent a WireGuard tunnel
configuration.
Here's an example .mobileconfig with the above fields filled in:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadDisplayName</key>
<string>WireGuard Demo Configuration Profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.your-org.wireguard.FCC9BF80-C540-44C1-B243-521FDD1B2905</string>
<key>PayloadUUID</key>
<string>F346AAF4-53A2-4FA1-ACA3-EEE74DBED029</string>
<key>PayloadContent</key>
<array>
<!-- An array of WireGuard configuration payload dictionaries -->
</array>
</dict>
</plist>
```
### WireGuard payload entries
Each WireGuard configuration payload dictionary should contain the following
keys:
- `PayloadDisplayName` (string): Should be `VPN`
- `PayloadType` (string): Should be `com.apple.vpn.managed`
- `PayloadVersion` (integer): Should be `1`
- `PayloadIdentifier` (string): A reverse-DNS style unique identifier for the WireGuard configuration profile.
- `PayloadUUID` (string): A randomly generated UUID for this payload
- `UserDefinedName` (string): The name of the WireGuard tunnel.
This name shall be used to represent the tunnel in the WireGuard app, and in the System UI for VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
- `VPNType` (string): Should be `VPN`
- `VPNSubType` (string): Should be set as the bundle identifier of the WireGuard app.
- iOS: `com.wireguard.ios`
- macOS: `com.wireguard.macos`
- `VendorConfig` (dict): Should be a dictionary with the following key:
- `WgQuickConfig` (string): Should be a WireGuard configuration in [wg-quick(8)] / [wg(8)] format.
The keys 'FwMark', 'Table', 'PreUp', 'PostUp', 'PreDown', 'PostDown' and 'SaveConfig' are not supported.
- `VPN` (dict): Should be a dictionary with the following keys:
- `RemoteAddress` (string): A non-empty string.
This string is displayed as the server name in the System UI for
VPNs (Settings > VPN on iOS, System Preferences > Network on macOS).
- `AuthenticationMethod` (string): Should be `Password`
Here's an example WireGuard configuration payload dictionary:
```xml
<!-- A WireGuard configuration payload dictionary -->
<dict>
<key>PayloadDisplayName</key>
<string>VPN</string>
<key>PayloadType</key>
<string>com.apple.vpn.managed</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadIdentifier</key>
<string>com.your-org.wireguard.demo-profile-1.demo-tunnel</string>
<key>PayloadUUID</key>
<string>44CDFE9F-4DC7-472A-956F-61C68055117C</string>
<key>UserDefinedName</key>
<string>Demo from MobileConfig file</string>
<key>VPNType</key>
<string>VPN</string>
<key>VPNSubType</key>
<string>com.wireguard.ios</string>
<key>VendorConfig</key>
<dict>
<key>WgQuickConfig</key>
<string>
[Interface]
PrivateKey = mInDaw06K0NgfULRObHJjkWD3ahUC8XC1tVjIf6W+Vo=
Address = 10.10.1.0/24
DNS = 1.1.1.1, 1.0.0.1
[Peer]
PublicKey = JRI8Xc0zKP9kXk8qP84NdUQA04h6DLfFbwJn4g+/PFs=
Endpoint = demo.wireguard.com:12912
AllowedIPs = 0.0.0.0/0
</string>
</dict>
<key>VPN</key>
<dict>
<key>RemoteAddress</key>
<string>demo.wireguard.com:12912</string>
<key>AuthenticationMethod</key>
<string>Password</string>
</dict>
</dict>
```
### Caveats
Configurations added via .mobileconfig will not be migrated into keychain until the WireGuard application is opened once.
[wg-quick(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8
[wg(8)]: https://git.zx2c4.com/WireGuard/about/src/tools/man/wg.8

View File

@ -1,21 +1,14 @@
# [WireGuard](https://www.wireguard.com/) for iOS
# [WireGuard](https://www.wireguard.com/) for iOS and macOS
## Building
- Clone this repo:
- Clone this repo recursively:
```
$ git clone https://git.zx2c4.com/wireguard-ios
$ git clone --recursive https://git.zx2c4.com/wireguard-ios
$ cd wireguard-ios
```
- Init and update submodule:
```
$ git submodule init
$ git submodule update
```
- Rename and populate developer team ID file:
```
@ -23,13 +16,19 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
$ vim WireGuard/WireGuard/Config/Developer.xcconfig
```
- Open project in XCode:
- Install swiftlint and go:
```
$ brew install swiftlint go
```
- Open project in Xcode:
```
$ open ./WireGuard/WireGuard.xcodeproj
```
- Flip switches, press buttons, and make whirling noises until XCode builds it.
- Flip switches, press buttons, and make whirling noises until Xcode builds it.
## MIT License

25
WireGuard/.swiftlint.yml Normal file
View File

@ -0,0 +1,25 @@
disabled_rules:
- line_length
- trailing_whitespace
- todo
- cyclomatic_complexity
- file_length
- type_body_length
- function_body_length
- nesting
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
identifier_name:
min_length:
warning: 0

View File

@ -1,27 +1,44 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import os.log
extension FileManager {
static var networkExtensionLogFileURL: 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)
static var appGroupId: String? {
#if os(iOS)
let appGroupIdInfoDictionaryKey = "com.wireguard.ios.app_group_id"
#elseif os(macOS)
let appGroupIdInfoDictionaryKey = "com.wireguard.macos.app_group_id"
#else
#error("Unimplemented")
#endif
return Bundle.main.object(forInfoDictionaryKey: appGroupIdInfoDictionaryKey) as? String
}
private static var sharedFolderURL: URL? {
guard let appGroupId = FileManager.appGroupId else {
os_log("Cannot obtain app group ID from bundle", log: OSLog.default, type: .error)
return nil
}
guard let sharedFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
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 logFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
}
static var networkExtensionLastErrorFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("last-error.txt")
}
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

View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import Security
class Keychain {
static func openReference(called ref: Data) -> String? {
var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: ref,
kSecReturnData as String: true] as CFDictionary,
&result)
if ret != errSecSuccess || result == nil {
wg_log(.error, message: "Unable to open config from keychain: \(ret)")
return nil
}
guard let data = result as? Data else { return nil }
return String(data: data, encoding: String.Encoding.utf8)
}
static func makeReference(containing value: String, called name: String, previouslyReferencedBy oldRef: Data? = nil) -> Data? {
var ret: OSStatus
guard var id = Bundle.main.bundleIdentifier else {
wg_log(.error, staticMessage: "Unable to determine bundle identifier")
return nil
}
if id.hasSuffix(".network-extension") {
id.removeLast(".network-extension".count)
}
var items: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
kSecAttrLabel as String: "WireGuard Tunnel: " + name,
kSecAttrAccount as String: name + ": " + UUID().uuidString,
kSecAttrDescription as String: "wg-quick(8) config",
kSecAttrService as String: id,
kSecValueData as String: value.data(using: .utf8) as Any,
kSecReturnPersistentRef as String: true]
#if os(iOS)
items[kSecAttrAccessGroup as String] = FileManager.appGroupId
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
#elseif os(macOS)
items[kSecAttrSynchronizable as String] = false
items[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
guard let extensionPath = Bundle.main.builtInPlugInsURL?.appendingPathComponent("WireGuardNetworkExtension.appex").path else {
wg_log(.error, staticMessage: "Unable to determine app extension path")
return nil
}
var extensionApp: SecTrustedApplication?
var mainApp: SecTrustedApplication?
ret = SecTrustedApplicationCreateFromPath(extensionPath, &extensionApp)
if ret != kOSReturnSuccess || extensionApp == nil {
wg_log(.error, message: "Unable to create keychain extension trusted application object: \(ret)")
return nil
}
ret = SecTrustedApplicationCreateFromPath(nil, &mainApp)
if ret != errSecSuccess || mainApp == nil {
wg_log(.error, message: "Unable to create keychain local trusted application object: \(ret)")
return nil
}
var access: SecAccess?
ret = SecAccessCreate((items[kSecAttrLabel as String] as? String)! as CFString,
[extensionApp!, mainApp!] as CFArray,
&access)
if ret != errSecSuccess || access == nil {
wg_log(.error, message: "Unable to create keychain ACL object: \(ret)")
return nil
}
items[kSecAttrAccess as String] = access!
#else
#error("Unimplemented")
#endif
var ref: CFTypeRef?
ret = SecItemAdd(items as CFDictionary, &ref)
if ret != errSecSuccess || ref == nil {
wg_log(.error, message: "Unable to add config to keychain: \(ret)")
return nil
}
if let oldRef = oldRef {
deleteReference(called: oldRef)
}
return ref as? Data
}
static func deleteReference(called ref: Data) {
let ret = SecItemDelete([kSecValuePersistentRef as String: ref] as CFDictionary)
if ret != errSecSuccess {
wg_log(.error, message: "Unable to delete config from keychain: \(ret)")
}
}
static func deleteReferences(except whitelist: Set<Data>) {
var result: CFTypeRef?
let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
kSecMatchLimit as String: kSecMatchLimitAll,
kSecReturnPersistentRef as String: true] as CFDictionary,
&result)
if ret != errSecSuccess || result == nil {
return
}
guard let items = result as? [Data] else { return }
for item in items {
if !whitelist.contains(item) {
deleteReference(called: item)
}
}
}
static func verifyReference(called ref: Data) -> Bool {
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: ref] as CFDictionary,
nil) == errSecSuccess
}
}

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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
var tag: String
init(tagged tag: String, withFilePath filePath: String) throws {
guard let log = open_log(filePath) else { throw LoggerError.openFailure }
self.log = log
self.tag = tag
}
deinit {
close_log(self.log)
}
func log(message: String) {
write_msg_to_log(log, tag, message.trimmingCharacters(in: .newlines))
}
func writeLog(to targetFile: String) -> Bool {
return write_log_to_file(targetFile, self.log) == 0
}
static func configureGlobal(tagged tag: String, 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
}
guard let logger = try? Logger(tagged: tag, withFilePath: filePath) else {
os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
return
}
Logger.global = logger
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)
}

View File

@ -0,0 +1,173 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
*/
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdatomic.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 = 2048,
MAGIC = 0xabadbeefU
};
struct log_line {
atomic_uint_fast64_t time_ns;
char line[MAX_LOG_LINE_LENGTH];
};
struct log {
atomic_uint_fast32_t next_index;
struct log_line lines[MAX_LINES];
uint32_t magic;
};
void write_msg_to_log(struct log *log, const char *tag, const char *msg)
{
uint32_t index;
struct log_line *line;
struct timespec ts;
// Race: This isn't synchronized with the fetch_add below, so items might be slightly out of order.
clock_gettime(CLOCK_REALTIME, &ts);
// Race: More than MAX_LINES writers and this will clash.
index = atomic_fetch_add(&log->next_index, 1);
line = &log->lines[index % MAX_LINES];
// Race: Before this line executes, we'll display old data after new data.
atomic_store(&line->time_ns, 0);
memset(line->line, 0, MAX_LOG_LINE_LENGTH);
snprintf(line->line, MAX_LOG_LINE_LENGTH, "[%s] %s", tag, msg);
atomic_store(&line->time_ns, ts.tv_sec * 1000000000ULL + ts.tv_nsec);
msync(&log->next_index, sizeof(log->next_index), MS_ASYNC);
msync(line, sizeof(*line), MS_ASYNC);
}
int write_log_to_file(const char *file_name, const struct log *input_log)
{
struct log *log;
uint32_t l, i;
FILE *file;
int ret;
log = malloc(sizeof(*log));
if (!log)
return -errno;
memcpy(log, input_log, sizeof(*log));
file = fopen(file_name, "w");
if (!file) {
free(log);
return -errno;
}
for (l = 0, i = log->next_index; l < MAX_LINES; ++l, ++i) {
const struct log_line *line = &log->lines[i % MAX_LINES];
time_t seconds = line->time_ns / 1000000000ULL;
uint32_t useconds = (line->time_ns % 1000000000ULL) / 1000ULL;
struct tm tm;
if (!line->time_ns)
continue;
if (!localtime_r(&seconds, &tm))
goto err;
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: %s\n",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, useconds,
line->line) < 0)
goto err;
}
errno = 0;
err:
ret = -errno;
fclose(file);
free(log);
return ret;
}
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*cb)(const char *, uint64_t))
{
struct log *log;
uint32_t l, i = cursor;
log = malloc(sizeof(*log));
if (!log)
return cursor;
memcpy(log, input_log, sizeof(*log));
if (i == -1)
i = log->next_index;
for (l = 0; l < MAX_LINES; ++l, ++i) {
const struct log_line *line = &log->lines[i % MAX_LINES];
if (cursor != -1 && i % MAX_LINES == log->next_index % MAX_LINES)
break;
if (!line->time_ns) {
if (cursor == -1)
continue;
else
break;
}
cb(line->line, line->time_ns);
cursor = (i + 1) % MAX_LINES;
}
free(log);
return cursor;
}
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));
}

View File

@ -0,0 +1,18 @@
/* SPDX-License-Identifier: MIT
*
* Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
*/
#ifndef RINGLOGGER_H
#define RINGLOGGER_H
#include <stdint.h>
struct log;
void write_msg_to_log(struct log *log, const char *tag, const char *msg);
int write_log_to_file(const char *file_name, const struct log *input_log);
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void(*)(const char *, uint64_t));
struct log *open_log(const char *file_name);
void close_log(struct log *log);
#endif

View File

@ -0,0 +1,63 @@
#include "ringlogger.h"
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <inttypes.h>
#include <sys/wait.h>
static void forkwrite(void)
{
struct log *log = open_log("/tmp/test_log");
char c[512];
int i, base;
bool in_fork = !fork();
base = 10000 * in_fork;
for (i = 0; i < 1024; ++i) {
snprintf(c, 512, "bla bla bla %d", base + i);
write_msg_to_log(log, "HMM", c);
}
if (in_fork)
_exit(0);
wait(NULL);
write_log_to_file("/dev/stdout", log);
close_log(log);
}
static void writetext(const char *text)
{
struct log *log = open_log("/tmp/test_log");
write_msg_to_log(log, "TXT", text);
close_log(log);
}
static void show_line(const char *line, uint64_t time_ns)
{
printf("%" PRIu64 ": %s\n", time_ns, line);
}
static void follow(void)
{
uint32_t cursor = -1;
struct log *log = open_log("/tmp/test_log");
for (;;) {
cursor = view_lines_from_cursor(log, cursor, show_line);
usleep(1000 * 300);
}
}
int main(int argc, char *argv[])
{
if (!strcmp(argv[1], "fork"))
forkwrite();
else if (!strcmp(argv[1], "write"))
writetext(argv[2]);
else if (!strcmp(argv[1], "follow"))
follow();
return 0;
}

View File

@ -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") }
}
}

View File

@ -1,18 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
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
}
}

View File

@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
extension Data {
func isKey() -> Bool {
return self.count == WG_KEY_LEN
}
func hexKey() -> String? {
if self.count != WG_KEY_LEN {
return nil
}
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_HEX))
out.withUnsafeMutableBytes { outBytes in
self.withUnsafeBytes { inBytes in
key_to_hex(outBytes, inBytes)
}
}
out.removeLast()
return String(data: out, encoding: .ascii)
}
init?(hexKey hexString: String) {
self.init(repeating: 0, count: Int(WG_KEY_LEN))
if !self.withUnsafeMutableBytes { key_from_hex($0, hexString) } {
return nil
}
}
func base64Key() -> String? {
if self.count != WG_KEY_LEN {
return nil
}
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_BASE64))
out.withUnsafeMutableBytes { outBytes in
self.withUnsafeBytes { inBytes in
key_to_base64(outBytes, inBytes)
}
}
out.removeLast()
return String(data: out, encoding: .ascii)
}
init?(base64Key base64String: String) {
self.init(repeating: 0, count: Int(WG_KEY_LEN))
if !self.withUnsafeMutableBytes { key_from_base64($0, base64String) } {
return nil
}
}
}

View File

@ -1,31 +1,56 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
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
}
}

View File

@ -1,20 +1,44 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
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)
}
}

View File

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

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum PacketTunnelProviderError: String, Error {
case savedProtocolConfigurationIsInvalid
case dnsResolutionFailure
case couldNotStartBackend
case couldNotDetermineFileDescriptor
case couldNotSetNetworkSettings
}
extension NETunnelProviderProtocol {
convenience init?(tunnelConfiguration: TunnelConfiguration, previouslyFrom old: NEVPNProtocol? = nil) {
self.init()
guard let name = tunnelConfiguration.name else { return nil }
guard let appId = Bundle.main.bundleIdentifier else { return nil }
providerBundleIdentifier = "\(appId).network-extension"
passwordReference = Keychain.makeReference(containing: tunnelConfiguration.asWgQuickConfig(), called: name, previouslyReferencedBy: old?.passwordReference)
if passwordReference == nil {
return nil
}
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? {
if let passwordReference = passwordReference,
let config = Keychain.openReference(called: passwordReference) {
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
}
if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
return try? TunnelConfiguration(fromWgQuickConfig: oldConfig, called: name)
}
return nil
}
func destroyConfigurationReference() {
guard let ref = passwordReference else { return }
Keychain.deleteReference(called: ref)
}
func verifyConfigurationReference() -> Data? {
guard let ref = passwordReference else { return nil }
return Keychain.verifyReference(called: ref) ? ref : nil
}
@discardableResult
func migrateConfigurationIfNeeded(called name: String) -> Bool {
/* This is how we did things before we switched to putting items
* in the keychain. But it's still useful to keep the migration
* around so that .mobileconfig files are easier.
*/
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false }
providerConfiguration = nil
guard passwordReference == nil else { return true }
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
return true
}
}

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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?
var rxBytes: UInt64?
var txBytes: UInt64?
var lastHandshakeTime: Date?
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)
}
}

View File

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

View File

@ -0,0 +1,251 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
extension TunnelConfiguration {
enum ParserState {
case inInterfaceSection
case inPeerSection
case notInASection
}
enum ParseError: Error {
case invalidLine(String.SubSequence)
case noInterface
case multipleInterfaces
case interfaceHasNoPrivateKey
case interfaceHasInvalidPrivateKey(String)
case interfaceHasInvalidListenPort(String)
case interfaceHasInvalidAddress(String)
case interfaceHasInvalidDNS(String)
case interfaceHasInvalidMTU(String)
case interfaceHasUnrecognizedKey(String)
case peerHasNoPublicKey
case peerHasInvalidPublicKey(String)
case peerHasInvalidPreSharedKey(String)
case peerHasInvalidAllowedIP(String)
case peerHasInvalidEndpoint(String)
case peerHasInvalidPersistentKeepAlive(String)
case peerHasInvalidTransferBytes(String)
case peerHasInvalidLastHandshakeTime(String)
case peerHasUnrecognizedKey(String)
case multiplePeersWithSamePublicKey
case multipleEntriesForKey(String)
}
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: .whitespacesAndNewlines)
let lowercasedLine = trimmedLine.lowercased()
if !trimmedLine.isEmpty {
if let equalsIndex = trimmedLine.firstIndex(of: "=") {
// Line contains an attribute
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
let key = keyWithCase.lowercased()
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
if let presentValue = attributes[key] {
if keysWithMultipleEntriesAllowed.contains(key) {
attributes[key] = presentValue + "," + value
} else {
throw ParseError.multipleEntriesForKey(keyWithCase)
}
} else {
attributes[key] = value
}
let interfaceSectionKeys: Set<String> = ["privatekey", "listenport", "address", "dns", "mtu"]
let peerSectionKeys: Set<String> = ["publickey", "presharedkey", "allowedips", "endpoint", "persistentkeepalive"]
if parserState == .inInterfaceSection {
guard interfaceSectionKeys.contains(key) else {
throw ParseError.interfaceHasUnrecognizedKey(keyWithCase)
}
} else if parserState == .inPeerSection {
guard peerSectionKeys.contains(key) else {
throw ParseError.peerHasUnrecognizedKey(keyWithCase)
}
}
} 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 {
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
} else if parserState == .inPeerSection {
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
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"
if let privateKey = interface.privateKey.base64Key() {
output.append("PrivateKey = \(privateKey)\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")
if let publicKey = peer.publicKey.base64Key() {
output.append("PublicKey = \(publicKey)\n")
}
if let preSharedKey = peer.preSharedKey?.base64Key() {
output.append("PresharedKey = \(preSharedKey)\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
}
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
guard let privateKeyString = attributes["privatekey"] else {
throw ParseError.interfaceHasNoPrivateKey
}
guard let privateKey = Data(base64Key: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
}
var interface = InterfaceConfiguration(privateKey: privateKey)
if let listenPortString = attributes["listenport"] {
guard let listenPort = UInt16(listenPortString) else {
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
}
interface.listenPort = listenPort
}
if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]()
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let address = IPAddressRange(from: addressString) else {
throw ParseError.interfaceHasInvalidAddress(addressString)
}
addresses.append(address)
}
interface.addresses = addresses
}
if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let dnsServer = DNSServer(from: dnsServerString) else {
throw ParseError.interfaceHasInvalidDNS(dnsServerString)
}
dnsServers.append(dnsServer)
}
interface.dns = dnsServers
}
if let mtuString = attributes["mtu"] {
guard let mtu = UInt16(mtuString) else {
throw ParseError.interfaceHasInvalidMTU(mtuString)
}
interface.mtu = mtu
}
return interface
}
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
guard let publicKeyString = attributes["publickey"] else {
throw ParseError.peerHasNoPublicKey
}
guard let publicKey = Data(base64Key: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
}
var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["presharedkey"] {
guard let preSharedKey = Data(base64Key: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
}
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 {
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
}
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else {
throw ParseError.peerHasInvalidEndpoint(endpointString)
}
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistentkeepalive"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
}
peer.persistentKeepAlive = persistentKeepAlive
}
return peer
}
}

View File

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

View File

@ -0,0 +1,114 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
* This is a specialized constant-time base64/hex implementation that resists side-channel attacks.
*/
#include <string.h>
#include "key.h"
static inline void encode_base64(char dest[static 4], const uint8_t src[static 3])
{
const uint8_t input[] = { (src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63 };
for (unsigned int i = 0; i < 4; ++i)
dest[i] = input[i] + 'A'
+ (((25 - input[i]) >> 8) & 6)
- (((51 - input[i]) >> 8) & 75)
- (((61 - input[i]) >> 8) & 15)
+ (((62 - input[i]) >> 8) & 3);
}
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN])
{
unsigned int i;
for (i = 0; i < WG_KEY_LEN / 3; ++i)
encode_base64(&base64[i * 4], &key[i * 3]);
encode_base64(&base64[i * 4], (const uint8_t[]){ key[i * 3 + 0], key[i * 3 + 1], 0 });
base64[WG_KEY_LEN_BASE64 - 2] = '=';
base64[WG_KEY_LEN_BASE64 - 1] = '\0';
}
static inline int decode_base64(const char src[static 4])
{
int val = 0;
for (unsigned int i = 0; i < 4; ++i)
val |= (-1
+ ((((('A' - 1) - src[i]) & (src[i] - ('Z' + 1))) >> 8) & (src[i] - 64))
+ ((((('a' - 1) - src[i]) & (src[i] - ('z' + 1))) >> 8) & (src[i] - 70))
+ ((((('0' - 1) - src[i]) & (src[i] - ('9' + 1))) >> 8) & (src[i] + 5))
+ ((((('+' - 1) - src[i]) & (src[i] - ('+' + 1))) >> 8) & 63)
+ ((((('/' - 1) - src[i]) & (src[i] - ('/' + 1))) >> 8) & 64)
) << (18 - 6 * i);
return val;
}
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64)
{
unsigned int i;
volatile uint8_t ret = 0;
int val;
if (strlen(base64) != WG_KEY_LEN_BASE64 - 1 || base64[WG_KEY_LEN_BASE64 - 2] != '=')
return false;
for (i = 0; i < WG_KEY_LEN / 3; ++i) {
val = decode_base64(&base64[i * 4]);
ret |= (uint32_t)val >> 31;
key[i * 3 + 0] = (val >> 16) & 0xff;
key[i * 3 + 1] = (val >> 8) & 0xff;
key[i * 3 + 2] = val & 0xff;
}
val = decode_base64((const char[]){ base64[i * 4 + 0], base64[i * 4 + 1], base64[i * 4 + 2], 'A' });
ret |= ((uint32_t)val >> 31) | (val & 0xff);
key[i * 3 + 0] = (val >> 16) & 0xff;
key[i * 3 + 1] = (val >> 8) & 0xff;
return 1 & ((ret - 1) >> 8);
}
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN])
{
unsigned int i;
for (i = 0; i < WG_KEY_LEN; ++i) {
hex[i * 2] = 87U + (key[i] >> 4) + ((((key[i] >> 4) - 10U) >> 8) & ~38U);
hex[i * 2 + 1] = 87U + (key[i] & 0xf) + ((((key[i] & 0xf) - 10U) >> 8) & ~38U);
}
hex[i * 2] = '\0';
}
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex)
{
uint8_t c, c_acc, c_alpha0, c_alpha, c_num0, c_num, c_val;
volatile uint8_t ret = 0;
if (strlen(hex) != WG_KEY_LEN_HEX - 1)
return false;
for (unsigned int i = 0; i < WG_KEY_LEN_HEX - 1; i += 2) {
c = (uint8_t)hex[i];
c_num = c ^ 48U;
c_num0 = (c_num - 10U) >> 8;
c_alpha = (c & ~32U) - 55U;
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
c_acc = c_val * 16U;
c = (uint8_t)hex[i + 1];
c_num = c ^ 48U;
c_num0 = (c_num - 10U) >> 8;
c_alpha = (c & ~32U) - 55U;
c_alpha0 = ((c_alpha - 10U) ^ (c_alpha - 16U)) >> 8;
ret |= ((c_num0 | c_alpha0) - 1) >> 8;
c_val = (c_num0 & c_num) | (c_alpha0 & c_alpha);
key[i / 2] = c_acc | c_val;
}
return 1 & ((ret - 1) >> 8);
}

View File

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
#ifndef KEY_H
#define KEY_H
#include <stdbool.h>
#include <stdint.h>
#define WG_KEY_LEN (32)
#define WG_KEY_LEN_BASE64 (45)
#define WG_KEY_LEN_HEX (65)
void key_to_base64(char base64[static WG_KEY_LEN_BASE64], const uint8_t key[static WG_KEY_LEN]);
bool key_from_base64(uint8_t key[static WG_KEY_LEN], const char *base64);
void key_to_hex(char hex[static WG_KEY_LEN_HEX], const uint8_t key[static WG_KEY_LEN]);
bool key_from_hex(uint8_t key[static WG_KEY_LEN], const char *hex);
#endif

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,6 @@
<dict>
<key>FILEHEADER</key>
<string> SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.</string>
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
</dict>
</plist>

View File

@ -0,0 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
// iOS permission prompts
NSCameraUsageDescription = "Camera is used for scanning QR codes for importing WireGuard configurations";
NSFaceIDUsageDescription = "Face ID is used for authenticating viewing and exporting of private keys";

View File

@ -0,0 +1,389 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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";
"tunnelsListSelectButtonTitle" = "Select";
"tunnelsListSelectAllButtonTitle" = "Select All";
"tunnelsListDeleteButtonTitle" = "Delete";
"tunnelsListSelectedTitle (%d)" = "%d selected";
// 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
"alertImportedFromMultipleFilesTitle (%d)" = "Created %d tunnels";
"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from imported files";
"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
"alertBadConfigImportTitle" = "Unable to import tunnel";
"alertBadConfigImportMessage (%@)" = "The file %@ does not contain a valid WireGuard configuration";
"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Delete %d tunnel?";
"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Delete %d tunnels?";
// 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";
"macToggleStatusButtonActivate" = "Activate";
"macToggleStatusButtonActivating" = "Activating…";
"macToggleStatusButtonDeactivate" = "Deactivate";
"macToggleStatusButtonDeactivating" = "Deactivating…";
"macToggleStatusButtonReasserting" = "Reactivating…";
"macToggleStatusButtonRestarting" = "Restarting…";
"macToggleStatusButtonWaiting" = "Waiting…";
"tunnelSectionTitleInterface" = "Interface";
"tunnelInterfaceName" = "Name";
"tunnelInterfacePrivateKey" = "Private key";
"tunnelInterfacePublicKey" = "Public key";
"tunnelInterfaceGenerateKeypair" = "Generate keypair";
"tunnelInterfaceAddresses" = "Addresses";
"tunnelInterfaceListenPort" = "Listen port";
"tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS servers";
"tunnelInterfaceStatus" = "Status";
"tunnelSectionTitlePeer" = "Peer";
"tunnelPeerPublicKey" = "Public key";
"tunnelPeerPreSharedKey" = "Preshared key";
"tunnelPeerEndpoint" = "Endpoint";
"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
"tunnelPeerAllowedIPs" = "Allowed IPs";
"tunnelPeerRxBytes" = "Data received";
"tunnelPeerTxBytes" = "Data sent";
"tunnelPeerLastHandshakeTime" = "Latest handshake";
"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
"tunnelSectionTitleOnDemand" = "On-Demand Activation";
"tunnelOnDemandCellular" = "Cellular";
"tunnelOnDemandEthernet" = "Ethernet";
"tunnelOnDemandWiFi" = "Wi-Fi";
"tunnelOnDemandSSIDsKey" = "SSIDs";
"tunnelOnDemandAnySSID" = "Any SSID";
"tunnelOnDemandOnlyTheseSSIDs" = "Only these SSIDs";
"tunnelOnDemandExceptTheseSSIDs" = "Except these SSIDs";
"tunnelOnDemandOnlySSID (%d)" = "Only %d SSID";
"tunnelOnDemandOnlySSIDs (%d)" = "Only %d SSIDs";
"tunnelOnDemandExceptSSID (%d)" = "Except %d SSID";
"tunnelOnDemandExceptSSIDs (%d)" = "Except %d SSIDs";
"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
"tunnelOnDemandSSIDViewTitle" = "SSIDs";
"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
"tunnelOnDemandNoSSIDs" = "No SSIDs";
"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
"tunnelOnDemandKey" = "On demand";
"tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
"tunnelOnDemandOptionCellularOnly" = "Cellular only";
"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi or ethernet";
"tunnelOnDemandOptionEthernetOnly" = "Ethernet 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";
"tunnelPeerPersistentKeepaliveValue (%@)" = "every %@ seconds";
"tunnelHandshakeTimestampNow" = "Now";
"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
"tunnelHandshakeTimestampAgo (%@)" = "%@ ago";
"tunnelHandshakeTimestampYear (%d)" = "%d year";
"tunnelHandshakeTimestampYears (%d)" = "%d years";
"tunnelHandshakeTimestampDay (%d)" = "%d day";
"tunnelHandshakeTimestampDays (%d)" = "%d days";
"tunnelHandshakeTimestampHour (%d)" = "%d hour";
"tunnelHandshakeTimestampHours (%d)" = "%d hours";
"tunnelHandshakeTimestampMinute (%d)" = "%d minute";
"tunnelHandshakeTimestampMinutes (%d)" = "%d minutes";
"tunnelHandshakeTimestampSecond (%d)" = "%d second";
"tunnelHandshakeTimestampSeconds (%d)" = "%d seconds";
"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours";
"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes";
"tunnelPeerPresharedKeyEnabled" = "enabled";
// 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" = "Interfaces private key is required";
"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interfaces 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" = "Interfaces listen port must be between 0 and 65535, or unspecified";
"alertInvalidInterfaceMessageMTUInvalid" = "Interfaces MTU must be between 576 and 65535, or unspecified";
"alertInvalidInterfaceMessageDNSInvalid" = "Interfaces 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" = "Peers public key is required";
"alertInvalidPeerMessagePublicKeyInvalid" = "Peers public key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peers preshared key must be a 32-byte key in base64 encoding";
"alertInvalidPeerMessageAllowedIPsInvalid" = "Peers allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
"alertInvalidPeerMessageEndpointInvalid" = "Peers endpoint must be of the form host:port or [host]:port";
"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peers 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";
"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.";
// Conf import error alerts
"alertCantOpenInputConfFileTitle" = "Unable to import from file";
"alertCantOpenInputConfFileMessage (%@)" = "The file %@ could not be read.";
// 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.";
// Mac status bar menu / pulldown menu
"macMenuNetworks (%@)" = "Networks: %@";
"macMenuNetworksNone" = "Networks: None";
"macMenuTitle" = "WireGuard";
"macMenuManageTunnels" = "Manage tunnels";
"macMenuImportTunnels" = "Import tunnel(s) from file…";
"macMenuAddEmptyTunnel" = "Add empty tunnel…";
"macMenuExportLog" = "Export log to file…";
"macMenuExportTunnels" = "Export tunnels to zip…";
"macMenuAbout" = "About WireGuard";
"macMenuQuit" = "Quit";
// Mac manage tunnels window
"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
"macDeleteTunnelConfirmationAlertMessage (%@)" = "Are you sure you want to delete %@?";
"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Are you sure you want to delete %d tunnels?";
"macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
"macButtonImportTunnels" = "Import tunnel(s) from file";
"macSheetButtonImport" = "Import";
"macNameFieldExportLog" = "Export log to";
"macSheetButtonExportLog" = "Save";
"macNameFieldExportZip" = "Export tunnels to";
"macSheetButtonExportZip" = "Save";
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
// Mac detail/edit view fields
"macFieldKey (%@)" = "%@:";
"macFieldOnDemand" = "On-Demand:";
"macFieldOnDemandSSIDs" = "SSIDs:";
// Mac status display
"macStatus (%@)" = "Status: %@";
// Mac editing config
"macEditDiscard" = "Discard";
"macEditSave" = "Save";
"macAlertNameIsEmpty" = "Name is required";
"macAlertDuplicateName (%@)" = "Another tunnel already exists with the name %@.";
"macAlertInvalidLine (%@)" = "Invalid line: %@.";
"macAlertNoInterface" = "Configuration must have an Interface section.";
"macAlertMultipleInterfaces" = "Configuration must have only one Interface section.";
"macAlertPrivateKeyInvalid" = "Private key is invalid.";
"macAlertListenPortInvalid (%@)" = "Listen port %@ is invalid.";
"macAlertAddressInvalid (%@)" = "Address %@ is invalid.";
"macAlertDNSInvalid (%@)" = "DNS %@ is invalid.";
"macAlertMTUInvalid (%@)" = "MTU %@ is invalid.";
"macAlertUnrecognizedInterfaceKey (%@)" = "Interface contains unrecognized key %@";
"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: PrivateKey, ListenPort, Address, DNS and MTU.";
"macAlertPublicKeyInvalid" = "Public key is invalid";
"macAlertPreSharedKeyInvalid" = "Preshared key is invalid";
"macAlertAllowedIPInvalid (%@)" = "Allowed IP %@ is invalid";
"macAlertEndpointInvalid (%@)" = "Endpoint %@ is invalid";
"macAlertPersistentKeepliveInvalid (%@)" = "Persistent keepalive value %@ is invalid";
"macAlertUnrecognizedPeerKey (%@)" = "Peer contains unrecognized key %@";
"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: PublicKey, PresharedKey, AllowedIPs, Endpoint and PersistentKeepalive";
"macAlertMultipleEntriesForKey (%@)" = "There should be only one entry per section for key %@";
// Mac about dialog
"macAppVersion (%@)" = "App version: %@";
"macGoBackendVersion (%@)" = "Go backend version: %@";
// Privacy
"macExportPrivateData" = "export tunnel private keys";
"macViewPrivateData" = "view tunnel private keys";
"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
// Mac alert
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
"macPrivacyNoticeMessage" = "Privacy notice: be sure you trust this configuration file";
"macPrivacyNoticeInfo" = "You will be prompted by the system to allow or disallow adding a VPN configuration. While this application does not send any information to the WireGuard project, information is by design sent to the servers specified inside of the configuration file you have just added, which configures your computer to use those servers as a VPN. Be certain that you trust this configuration before clicking “Allow” in the following dialog.";
// Mac tooltip
"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
"macToolTipToggleStatus" = "Toggle status (⌘T)";

View File

@ -3,7 +3,8 @@
// You Apple developer account's Team ID
DEVELOPMENT_TEAM = <team_id>
// The bundle identifier of this app.
// The bundle identifier of the apps.
// Should be an app id created at developer.apple.com
// with Network Extensions capabilty.
APP_ID = <app_id>
APP_ID_IOS = <app_id>
APP_ID_MACOS = <app_id>

View File

@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20181104
VERSION_NAME = 0.0.20190319
VERSION_ID = 5

View File

@ -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
}
}
}

View File

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

View File

@ -1,27 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
import Foundation
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
}
}

View File

@ -1,12 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0+
*
* Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
* Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*
* Curve25519 ECDH functions, based on TweetNaCl but cleaned up.
*/
#include <stdint.h>
#include <string.h>
#include <assert.h>
#include <CommonCrypto/CommonRandom.h>
#include "x25519.h"
@ -171,7 +172,7 @@ void curve25519_derive_public_key(uint8_t public_key[32], const uint8_t private_
void curve25519_generate_private_key(uint8_t private_key[32])
{
CCRandomGenerateBytes(private_key, 32);
assert(CCRandomGenerateBytes(private_key, 32) == kCCSuccess);
private_key[31] = (private_key[31] & 127) | 64;
private_key[0] &= 248;
}

View File

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

View File

@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension
enum ActivateOnDemandOption: Equatable {
case off
case wiFiInterfaceOnly(ActivateOnDemandSSIDOption)
case nonWiFiInterfaceOnly
case anyInterface(ActivateOnDemandSSIDOption)
}
#if os(iOS)
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .cellular
#elseif os(macOS)
private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
#else
#error("Unimplemented")
#endif
enum ActivateOnDemandSSIDOption: Equatable {
case anySSID
case onlySpecificSSIDs([String])
case exceptSpecificSSIDs([String])
}
extension ActivateOnDemandOption {
func apply(on tunnelProviderManager: NETunnelProviderManager) {
let rules: [NEOnDemandRule]?
switch self {
case .off:
rules = nil
case .wiFiInterfaceOnly(let ssidOption):
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleDisconnect(interfaceType: nonWiFiInterfaceType)]
case .nonWiFiInterfaceOnly:
rules = [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType), NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
case .anyInterface(let ssidOption):
if case .anySSID = ssidOption {
rules = [NEOnDemandRuleConnect(interfaceType: .any)]
} else {
rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType)]
}
}
tunnelProviderManager.onDemandRules = rules
tunnelProviderManager.isOnDemandEnabled = self != .off
}
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
let activateOnDemandOption: ActivateOnDemandOption
switch rules.count {
case 0:
activateOnDemandOption = .off
case 1:
let rule = rules[0]
precondition(rule.action == .connect)
activateOnDemandOption = .anyInterface(.anySSID)
case 2:
let connectRule = rules.first(where: { $0.action == .connect })!
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
activateOnDemandOption = .wiFiInterfaceOnly(.anySSID)
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = .nonWiFiInterfaceOnly
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
}
case 3:
let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil })!
let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType })!
let ssids = ssidRule.ssidMatch!
switch (ssidRule.action, nonWiFiRule.action) {
case (.connect, .connect):
activateOnDemandOption = .anyInterface(.onlySpecificSSIDs(ssids))
case (.connect, .disconnect):
activateOnDemandOption = .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
case (.disconnect, .connect):
activateOnDemandOption = .anyInterface(.exceptSpecificSSIDs(ssids))
case (.disconnect, .disconnect):
activateOnDemandOption = .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
default:
fatalError("Unexpected SSID onDemandRules set on tunnel provider manager")
}
default:
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self = activateOnDemandOption
}
}
private extension NEOnDemandRuleConnect {
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
self.init()
interfaceTypeMatch = interfaceType
ssidMatch = ssids
}
}
private extension NEOnDemandRuleDisconnect {
convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
self.init()
interfaceTypeMatch = interfaceType
ssidMatch = ssids
}
}
private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDemandRule] {
switch option {
case .anySSID:
return [NEOnDemandRuleConnect(interfaceType: .wiFi)]
case .onlySpecificSSIDs(let ssids):
assert(!ssids.isEmpty)
return [NEOnDemandRuleConnect(interfaceType: .wiFi, ssids: ssids),
NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
case .exceptSpecificSSIDs(let ssids):
return [NEOnDemandRuleDisconnect(interfaceType: .wiFi, ssids: ssids),
NEOnDemandRuleConnect(interfaceType: .wiFi)]
}
}

View File

@ -0,0 +1,48 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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

View File

@ -0,0 +1,184 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
extension TunnelConfiguration {
convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]()
var lines = uapiConfig.split(separator: "\n")
lines.append("")
var parserState = ParserState.inInterfaceSection
var attributes = [String: String]()
for line in lines {
var key = ""
var value = ""
if !line.isEmpty {
guard let equalsIndex = line.firstIndex(of: "=") else { throw ParseError.invalidLine(line) }
key = String(line[..<equalsIndex])
value = String(line[line.index(equalsIndex, offsetBy: 1)...])
}
if line.isEmpty || key == "public_key" {
// Previous section has ended; process the attributes collected so far
if parserState == .inInterfaceSection {
let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
interfaceConfiguration = interface
parserState = .inPeerSection
} else if parserState == .inPeerSection {
let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
peerConfigurations.append(peer)
}
attributes.removeAll()
if line.isEmpty {
break
}
}
if let presentValue = attributes[key] {
if key == "allowed_ip" {
attributes[key] = presentValue + "," + value
} else {
throw ParseError.multipleEntriesForKey(key)
}
} else {
attributes[key] = value
}
let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
let peerSectionKeys: Set<String> = ["public_key", "preshared_key", "allowed_ip", "endpoint", "persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec", "rx_bytes", "tx_bytes", "protocol_version"]
if parserState == .inInterfaceSection {
guard interfaceSectionKeys.contains(key) else {
throw ParseError.interfaceHasUnrecognizedKey(key)
}
}
if parserState == .inPeerSection {
guard peerSectionKeys.contains(key) else {
throw ParseError.peerHasUnrecognizedKey(key)
}
}
}
let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
let peerPublicKeysSet = Set<Data>(peerPublicKeysArray)
if peerPublicKeysArray.count != peerPublicKeysSet.count {
throw ParseError.multiplePeersWithSamePublicKey
}
interfaceConfiguration?.addresses = base?.interface.addresses ?? []
interfaceConfiguration?.dns = base?.interface.dns ?? []
interfaceConfiguration?.mtu = base?.interface.mtu
if let interfaceConfiguration = interfaceConfiguration {
self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
} else {
throw ParseError.noInterface
}
}
private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
guard let privateKeyString = attributes["private_key"] else {
throw ParseError.interfaceHasNoPrivateKey
}
guard let privateKey = Data(hexKey: privateKeyString), privateKey.count == TunnelConfiguration.keyLength else {
throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
}
var interface = InterfaceConfiguration(privateKey: privateKey)
if let listenPortString = attributes["listen_port"] {
guard let listenPort = UInt16(listenPortString) else {
throw ParseError.interfaceHasInvalidListenPort(listenPortString)
}
if listenPort != 0 {
interface.listenPort = listenPort
}
}
return interface
}
private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
guard let publicKeyString = attributes["public_key"] else {
throw ParseError.peerHasNoPublicKey
}
guard let publicKey = Data(hexKey: publicKeyString), publicKey.count == TunnelConfiguration.keyLength else {
throw ParseError.peerHasInvalidPublicKey(publicKeyString)
}
var peer = PeerConfiguration(publicKey: publicKey)
if let preSharedKeyString = attributes["preshared_key"] {
guard let preSharedKey = Data(hexKey: preSharedKeyString), preSharedKey.count == TunnelConfiguration.keyLength else {
throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
}
// TODO(zx2c4): does the compiler optimize this away?
var accumulator: UInt8 = 0
for index in 0..<preSharedKey.count {
accumulator |= preSharedKey[index]
}
if accumulator != 0 {
peer.preSharedKey = preSharedKey
}
}
if let allowedIPsString = attributes["allowed_ip"] {
var allowedIPs = [IPAddressRange]()
for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let allowedIP = IPAddressRange(from: allowedIPString) else {
throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
}
allowedIPs.append(allowedIP)
}
peer.allowedIPs = allowedIPs
}
if let endpointString = attributes["endpoint"] {
guard let endpoint = Endpoint(from: endpointString) else {
throw ParseError.peerHasInvalidEndpoint(endpointString)
}
peer.endpoint = endpoint
}
if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
}
if persistentKeepAlive != 0 {
peer.persistentKeepAlive = persistentKeepAlive
}
}
if let rxBytesString = attributes["rx_bytes"] {
guard let rxBytes = UInt64(rxBytesString) else {
throw ParseError.peerHasInvalidTransferBytes(rxBytesString)
}
if rxBytes != 0 {
peer.rxBytes = rxBytes
}
}
if let txBytesString = attributes["tx_bytes"] {
guard let txBytes = UInt64(txBytesString) else {
throw ParseError.peerHasInvalidTransferBytes(txBytesString)
}
if txBytes != 0 {
peer.txBytes = txBytes
}
}
if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
var lastHandshakeTimeSince1970: TimeInterval = 0
guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
}
if lastHandshakeTimeSec != 0 {
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
}
lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
}
peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
}
}
return peer
}
}

View File

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

View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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"
}
}
}

View File

@ -0,0 +1,587 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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, tunnel: TunnelContainer)
}
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?
private var configurationsObservationToken: AnyObject?
init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
startObservingTunnelStatuses()
startObservingTunnelConfigurations()
}
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
}
var tunnelManagers = managers ?? []
var refs: Set<Data> = []
for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol
if proto?.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") ?? false {
tunnelManager.saveToPreferences { _ in }
}
if let ref = proto?.verifyConfigurationReference() {
refs.insert(ref)
} else {
tunnelManager.removeFromPreferences { _ in }
tunnelManagers.remove(at: index)
}
}
Keychain.deleteReferences(except: refs)
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
}
#endif
}
func reload() {
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
guard let self = self else { return }
let loadedTunnelProviders = managers ?? []
for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
if !loadedTunnelProviders.contains(where: { $0.tunnelConfiguration == currentTunnel.tunnelConfiguration }) {
// Tunnel was deleted outside the app
self.tunnels.remove(at: index)
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
}
}
for loadedTunnelProvider in loadedTunnelProviders {
if let matchingTunnel = self.tunnels.first(where: { $0.tunnelConfiguration == loadedTunnelProvider.tunnelConfiguration }) {
matchingTunnel.tunnelProvider = loadedTunnelProvider
matchingTunnel.refreshStatus()
} else {
// Tunnel was added outside the app
if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
loadedTunnelProvider.saveToPreferences { _ in }
}
}
let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
self.tunnels.append(tunnel)
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
}
}
}
}
func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, 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.setTunnelConfiguration(tunnelConfiguration)
tunnelProviderManager.isEnabled = true
onDemandOption.apply(on: tunnelProviderManager)
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
return
}
guard let self = self else { return }
#if os(iOS)
// HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
// This is an ugly hack to reactivate the tunnel that has been deactivated like that.
if let activeTunnel = activeTunnel {
if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
self.startActivation(of: activeTunnel)
}
if activeTunnel.status == .active || activeTunnel.status == .activating {
activeTunnel.status = .restarting
}
}
#endif
let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnels.append(tunnel)
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel))
}
}
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
}
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful, lastError)
return
}
let tail = tunnelConfigurations.dropFirst()
add(tunnelConfiguration: head) { [weak self, tail] result in
DispatchQueue.main.async {
let numberSuccessful = numberSuccessful + (result.isSuccess ? 1 : 0)
let lastError = lastError ?? (result.error as? TunnelsManagerError)
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful, lastError: lastError, completionHandler: completionHandler)
}
}
}
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, 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
}
var isTunnelConfigurationChanged = false
if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
isTunnelConfigurationChanged = true
}
tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
onDemandOption.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else {
//TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
return
}
guard let self = self else { return }
if isNameChanged {
let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($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 isTunnelConfigurationChanged {
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.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
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, tunnel: tunnel)
}
completionHandler(nil)
}
}
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
}
private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
guard let head = tunnels.first else {
completionHandler(nil)
return
}
let tail = tunnels.dropFirst()
remove(tunnel: head) { [weak self, tail] error in
DispatchQueue.main.async {
if let error = error {
completionHandler(error)
} else {
self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
}
}
}
}
func numberOfTunnels() -> Int {
return tunnels.count
}
func tunnel(at index: Int) -> TunnelContainer {
return tunnels[index]
}
func index(of tunnel: TunnelContainer) -> Int? {
return tunnels.firstIndex(of: tunnel)
}
func tunnel(named tunnelName: String) -> TunnelContainer? {
return tunnels.first { $0.name == tunnelName }
}
func waitingTunnel() -> TunnelContainer? {
return tunnels.first { $0.status == .waiting }
}
func tunnelInOperation() -> TunnelContainer? {
if let waitingTunnelObject = waitingTunnel() {
return waitingTunnelObject
}
return tunnels.first { $0.status != .inactive }
}
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 tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
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 {
tunnel.startActivation(activationDelegate: self.activationDelegate)
return
}
tunnel.refreshStatus()
}
}
func startObservingTunnelConfigurations() {
configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
DispatchQueue.main.async { [weak self] in
// We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
// (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
// removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
// as a result of a tunnel getting added or removed outside of the app.
self?.reload()
}
}
}
static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
}
}
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: .common)
}
}
}
var activationAttemptId: String?
var activationTimer: Timer?
var deactivationTimer: Timer?
fileprivate var tunnelProvider: NETunnelProviderManager
var tunnelConfiguration: TunnelConfiguration? {
return tunnelProvider.tunnelConfiguration
}
var onDemandOption: ActivateOnDemandOption {
return ActivateOnDemandOption(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 getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
completionHandler(tunnelConfiguration)
return
}
guard nil != (try? session.sendProviderMessage(Data(bytes: [ 0 ]), responseHandler: {
guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
completionHandler(self.tunnelConfiguration)
return
}
completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
})) else {
completionHandler(tunnelConfiguration)
return
}
}
func refreshStatus() {
if status == .restarting {
return
}
status = TunnelStatus(from: tunnelProvider.connection.status)
isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
}
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() {
wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
(tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
}
extension NETunnelProviderManager {
private static var cachedConfigKey: UInt8 = 0
var tunnelConfiguration: TunnelConfiguration? {
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
return cached
}
let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
if config != nil {
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return config
}
func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
localizedDescription = tunnelConfiguration.name
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

View File

@ -0,0 +1,198 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
class ActivateOnDemandViewModel {
enum OnDemandField {
case onDemand
case nonWiFiInterface
case wiFiInterface
case ssid
var localizedUIString: String {
switch self {
case .onDemand:
return tr("tunnelOnDemandKey")
case .nonWiFiInterface:
#if os(iOS)
return tr("tunnelOnDemandCellular")
#elseif os(macOS)
return tr("tunnelOnDemandEthernet")
#else
#error("Unimplemented")
#endif
case .wiFiInterface: return tr("tunnelOnDemandWiFi")
case .ssid: return tr("tunnelOnDemandSSIDsKey")
}
}
}
enum OnDemandSSIDOption {
case anySSID
case onlySpecificSSIDs
case exceptSpecificSSIDs
var localizedUIString: String {
switch self {
case .anySSID: return tr("tunnelOnDemandAnySSID")
case .onlySpecificSSIDs: return tr("tunnelOnDemandOnlyTheseSSIDs")
case .exceptSpecificSSIDs: return tr("tunnelOnDemandExceptTheseSSIDs")
}
}
}
var isNonWiFiInterfaceEnabled = false
var isWiFiInterfaceEnabled = false
var selectedSSIDs = [String]()
var ssidOption: OnDemandSSIDOption = .anySSID
}
extension ActivateOnDemandViewModel {
convenience init(tunnel: TunnelContainer) {
self.init()
if tunnel.isActivateOnDemandEnabled {
switch tunnel.onDemandOption {
case .off:
break
case .wiFiInterfaceOnly(let onDemandSSIDOption):
isWiFiInterfaceEnabled = true
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
case .nonWiFiInterfaceOnly:
isNonWiFiInterfaceEnabled = true
case .anyInterface(let onDemandSSIDOption):
isWiFiInterfaceEnabled = true
isNonWiFiInterfaceEnabled = true
(ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
}
}
}
func toOnDemandOption() -> ActivateOnDemandOption {
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
case (false, false):
return .off
case (false, true):
return .nonWiFiInterfaceOnly
case (true, false):
return .wiFiInterfaceOnly(toSSIDOption())
case (true, true):
return .anyInterface(toSSIDOption())
}
}
}
extension ActivateOnDemandViewModel {
func isEnabled(field: OnDemandField) -> Bool {
switch field {
case .nonWiFiInterface:
return isNonWiFiInterfaceEnabled
case .wiFiInterface:
return isWiFiInterfaceEnabled
default:
return false
}
}
func setEnabled(field: OnDemandField, isEnabled: Bool) {
switch field {
case .nonWiFiInterface:
isNonWiFiInterfaceEnabled = isEnabled
case .wiFiInterface:
isWiFiInterfaceEnabled = isEnabled
default:
break
}
}
}
extension ActivateOnDemandViewModel {
var localizedInterfaceDescription: String {
switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
case (false, false):
return tr("tunnelOnDemandOptionOff")
case (true, false):
return tr("tunnelOnDemandOptionWiFiOnly")
case (false, true):
#if os(iOS)
return tr("tunnelOnDemandOptionCellularOnly")
#elseif os(macOS)
return tr("tunnelOnDemandOptionEthernetOnly")
#else
#error("Unimplemented")
#endif
case (true, true):
#if os(iOS)
return tr("tunnelOnDemandOptionWiFiOrCellular")
#elseif os(macOS)
return tr("tunnelOnDemandOptionWiFiOrEthernet")
#else
#error("Unimplemented")
#endif
}
}
var localizedSSIDDescription: String {
guard isWiFiInterfaceEnabled else { return "" }
switch ssidOption {
case .anySSID: return tr("tunnelOnDemandAnySSID")
case .onlySpecificSSIDs:
if selectedSSIDs.count == 1 {
return tr(format: "tunnelOnDemandOnlySSID (%d)", selectedSSIDs.count)
} else {
return tr(format: "tunnelOnDemandOnlySSIDs (%d)", selectedSSIDs.count)
}
case .exceptSpecificSSIDs:
if selectedSSIDs.count == 1 {
return tr(format: "tunnelOnDemandExceptSSID (%d)", selectedSSIDs.count)
} else {
return tr(format: "tunnelOnDemandExceptSSIDs (%d)", selectedSSIDs.count)
}
}
}
func fixSSIDOption() {
selectedSSIDs = uniquifiedNonEmptySelectedSSIDs()
if selectedSSIDs.isEmpty {
ssidOption = .anySSID
}
}
}
private extension ActivateOnDemandViewModel {
func ssidViewModel(from ssidOption: ActivateOnDemandSSIDOption) -> (OnDemandSSIDOption, [String]) {
switch ssidOption {
case .anySSID:
return (.anySSID, [])
case .onlySpecificSSIDs(let ssids):
return (.onlySpecificSSIDs, ssids)
case .exceptSpecificSSIDs(let ssids):
return (.exceptSpecificSSIDs, ssids)
}
}
func toSSIDOption() -> ActivateOnDemandSSIDOption {
switch ssidOption {
case .anySSID:
return .anySSID
case .onlySpecificSSIDs:
let ssids = uniquifiedNonEmptySelectedSSIDs()
return ssids.isEmpty ? .anySSID : .onlySpecificSSIDs(selectedSSIDs)
case .exceptSpecificSSIDs:
let ssids = uniquifiedNonEmptySelectedSSIDs()
return ssids.isEmpty ? .anySSID : .exceptSpecificSSIDs(selectedSSIDs)
}
}
func uniquifiedNonEmptySelectedSSIDs() -> [String] {
let nonEmptySSIDs = selectedSSIDs.filter { !$0.isEmpty }
var seenSSIDs = Set<String>()
var uniquified = [String]()
for ssid in nonEmptySSIDs {
guard !seenSSIDs.contains(ssid) else { continue }
uniquified.append(ssid)
seenSSIDs.insert(ssid)
}
return uniquified
}
}

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
protocol ErrorPresenterProtocol {
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
}
extension ErrorPresenterProtocol {
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?) {
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: nil)
}
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onDismissal: (() -> Void)?) {
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: onDismissal)
}
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?) {
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: nil)
}
static func showErrorAlert(error: WireGuardAppError, from sourceVC: AnyObject?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
let (title, message) = error.alertText
showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
}
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
import LocalAuthentication
#if os(macOS)
import AppKit
#endif
class PrivateDataConfirmation {
static func confirmAccess(to reason: String, _ after: @escaping () -> Void) {
let context = LAContext()
var error: NSError?
if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
guard let error = error as? LAError else { return }
if error.code == .passcodeNotSet {
// We give no protection to folks who just don't set a passcode.
after()
}
return
}
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
DispatchQueue.main.async {
#if os(macOS)
if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
}
#endif
if success {
after()
}
}
}
}
}

View File

@ -0,0 +1,84 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
class TunnelImporter {
static func importFromFile(urls: [URL], into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) {
guard !urls.isEmpty else {
completionHandler?()
return
}
let dispatchGroup = DispatchGroup()
var configs = [TunnelConfiguration?]()
var lastFileImportErrorText: (title: String, message: String)?
for url in urls {
if url.pathExtension.lowercased() == "zip" {
dispatchGroup.enter()
ZipImporter.importConfigFiles(from: url) { result in
if let error = result.error {
lastFileImportErrorText = error.alertText
}
if let configsInZip = result.value {
configs.append(contentsOf: configsInZip)
}
dispatchGroup.leave()
}
} else { /* if it is not a zip, we assume it is a conf */
let fileName = url.lastPathComponent
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
dispatchGroup.enter()
DispatchQueue.global(qos: .userInitiated).async {
let fileContents: String
do {
fileContents = try String(contentsOf: url)
} catch let error {
DispatchQueue.main.async {
if let cocoaError = error as? CocoaError, cocoaError.isFileError {
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: error.localizedDescription)
} else {
lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: tr(format: "alertCantOpenInputConfFileMessage (%@)", fileName))
}
configs.append(nil)
dispatchGroup.leave()
}
return
}
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
DispatchQueue.main.async {
if tunnelConfiguration == nil {
lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
}
configs.append(tunnelConfiguration)
dispatchGroup.leave()
}
}
}
}
dispatchGroup.notify(queue: .main) {
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { numberSuccessful, lastAddError in
if !configs.isEmpty && numberSuccessful == configs.count {
completionHandler?()
return
}
let alertText: (title: String, message: String)?
if urls.count == 1 {
if urls.first!.pathExtension.lowercased() == "zip" && !configs.isEmpty {
alertText = (title: tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful),
message: tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count))
} else {
alertText = lastFileImportErrorText ?? lastAddError?.alertText
}
} else {
alertText = (title: tr(format: "alertImportedFromMultipleFilesTitle (%d)", numberSuccessful),
message: tr(format: "alertImportedFromMultipleFilesMessage (%1$d of %2$d)", numberSuccessful, configs.count))
}
if let alertText = alertText {
errorPresenterType.showErrorAlert(title: alertText.title, message: alertText.message, from: sourceVC, onPresented: completionHandler)
} else {
completionHandler?()
}
}
}
}
}

View File

@ -1,33 +1,68 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
import Foundation
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: CaseIterable {
case name
case privateKey
case publicKey
case generateKeyPair
case addresses
case listenPort
case mtu
case dns
case status
case toggleStatus
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")
case .status: return tr("tunnelInterfaceStatus")
case .toggleStatus: return ""
}
}
}
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: CaseIterable {
case publicKey
case preSharedKey
case endpoint
case persistentKeepAlive
case allowedIPs
case rxBytes
case txBytes
case lastHandshakeTime
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 .rxBytes: return tr("tunnelPeerRxBytes")
case .txBytes: return tr("tunnelPeerTxBytes")
case .lastHandshakeTime: return tr("tunnelPeerLastHandshakeTime")
case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
case .deletePeer: return tr("deletePeerButtonTitle")
}
}
}
static let peerFieldsWithControl: Set<PeerField> = [
@ -36,38 +71,47 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44
struct Changes {
enum FieldChange: Equatable {
case added
case removed
case modified(newValue: String)
}
var interfaceChanges: [InterfaceField: FieldChange]
var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
var peersRemovedIndices: [Int]
var peersInsertedIndices: [Int]
}
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 {
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey)
scratchpad[.publicKey] = publicKey.base64EncodedString()
if field == .privateKey {
if stringValue.count == TunnelViewModel.keyLengthInBase64, let privateKey = Data(base64Key: stringValue), privateKey.count == TunnelConfiguration.keyLength {
let publicKey = Curve25519.generatePublicKey(fromPrivateKey: privateKey).base64Key() ?? ""
scratchpad[.publicKey] = publicKey
} else {
scratchpad.removeValue(forKey: .publicKey)
}
@ -76,13 +120,18 @@ class TunnelViewModel {
}
func populateScratchpad() {
// Populate the scratchpad from the configuration object
guard let config = validatedConfiguration else { return }
scratchpad[.name] = config.name
scratchpad[.privateKey] = config.privateKey.base64EncodedString()
scratchpad[.publicKey] = config.publicKey.base64EncodedString()
if (!config.addresses.isEmpty) {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation() }.joined(separator: ", ")
guard let name = validatedName else { return }
scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
}
private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
var scratchpad = [InterfaceField: String]()
scratchpad[.name] = name
scratchpad[.privateKey] = config.privateKey.base64Key() ?? ""
scratchpad[.publicKey] = config.publicKey.base64Key() ?? ""
if !config.addresses.isEmpty {
scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
}
if let listenPort = config.listenPort {
scratchpad[.listenPort] = String(listenPort)
@ -90,40 +139,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: ", ")
}
return scratchpad
}
func save() -> SaveResult<InterfaceConfiguration> {
if let validatedConfiguration = validatedConfiguration {
// It's already validated and saved
return .saved(validatedConfiguration)
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(base64Key: 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 +181,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 +189,82 @@ 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
}
func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
if scratchpad.isEmpty {
populateScratchpad()
}
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
var changes = [InterfaceField: Changes.FieldChange]()
for field in InterfaceField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""):
break
case ("", _):
changes[field] = .added
case (_, ""):
changes[field] = .removed
case (let this, let other):
if this != other {
changes[field] = .modified(newValue: other)
}
}
}
scratchpad = otherScratchPad
return changes
}
}
class PeerData {
var index: Int
var scratchpad: [PeerField: String] = [:]
var fieldsWithError: Set<PeerField> = []
var scratchpad = [PeerField: String]()
var fieldsWithError = Set<PeerField>()
var validatedConfiguration: PeerConfiguration?
var publicKey: Data? {
if let validatedConfiguration = validatedConfiguration {
return validatedConfiguration.publicKey
}
if let scratchPadPublicKey = scratchpad[.publicKey] {
return Data(base64Key: scratchPadPublicKey)
}
return nil
}
// 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,83 +272,93 @@ 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()
scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
updateExcludePrivateIPsFieldState()
}
private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
var scratchpad = [PeerField: String]()
if let publicKey = config.publicKey.base64Key() {
scratchpad[.publicKey] = publicKey
}
if (!config.allowedIPs.isEmpty) {
scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ")
if let preSharedKey = config.preSharedKey?.base64Key() {
scratchpad[.preSharedKey] = preSharedKey
}
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)
}
updateExcludePrivateIPsFieldState()
if let rxBytes = config.rxBytes {
scratchpad[.rxBytes] = prettyBytes(rxBytes)
}
if let txBytes = config.txBytes {
scratchpad[.txBytes] = prettyBytes(txBytes)
}
if let lastHandshakeTime = config.lastHandshakeTime {
scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
}
return scratchpad
}
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(base64Key: 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(base64Key: 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 +368,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 +376,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"
@ -318,69 +405,95 @@ class TunnelViewModel {
"193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
]
func updateExcludePrivateIPsFieldState() {
guard (numberOfPeers == 1) else {
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false
return
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
guard isSinglePeer else {
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
}
if (scratchpad.isEmpty) {
populateScratchpad()
}
let allowedIPStrings = Set<String>(
(scratchpad[.allowedIPs] ?? "")
.split(separator: ",")
.map { $0.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) }
)
if (allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString)) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = false
} else if (allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)) {
shouldAllowExcludePrivateIPsControl = true
excludePrivateIPsValue = true
let allowedIPStrings = Set<String>(allowedIPs)
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: false)
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: true)
} else {
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
}
}
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 ipv6Addresses = allowedIPStrings.filter { $0.contains(":") }
let modifiedAllowedIPStrings: [String]
if (isOn) {
modifiedAllowedIPStrings = ipv6Addresses +
TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
} else {
modifiedAllowedIPStrings = ipv6Addresses +
[TunnelViewModel.PeerData.ipv4DefaultRouteString]
func updateExcludePrivateIPsFieldState() {
if scratchpad.isEmpty {
populateScratchpad()
}
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
(shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: numberOfPeers == 1, allowedIPs: allowedIPStrings)
shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
}
static func normalizedIPAddressRangeStrings(_ list: [String]) -> [String] {
return list.compactMap { IPAddressRange(from: $0) }.map { $0.stringRepresentation }
}
static func modifiedAllowedIPs(currentAllowedIPs: [String], excludePrivateIPs: Bool, dnsServers: [String], oldDNSServers: [String]?) -> [String] {
let normalizedDNSServers = normalizedIPAddressRangeStrings(dnsServers)
let normalizedOldDNSServers = oldDNSServers == nil ? normalizedDNSServers : normalizedIPAddressRangeStrings(oldDNSServers!)
let ipv6Addresses = normalizedIPAddressRangeStrings(currentAllowedIPs.filter { $0.contains(":") })
if excludePrivateIPs {
return ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + normalizedDNSServers
} else {
return ipv6Addresses.filter { !normalizedOldDNSServers.contains($0) } + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
}
}
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String, oldDNSServers: String? = nil) {
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let oldDNSServerStrings = oldDNSServers?.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let modifiedAllowedIPStrings = TunnelViewModel.PeerData.modifiedAllowedIPs(currentAllowedIPs: allowedIPStrings, excludePrivateIPs: isOn, dnsServers: dnsServerStrings, oldDNSServers: oldDNSServerStrings)
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil // The configuration has been modified, and needs to be saved
validatedConfiguration = nil
excludePrivateIPsValue = isOn
}
func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
if scratchpad.isEmpty {
populateScratchpad()
}
let otherScratchPad = PeerData.createScratchPad(from: other)
var changes = [PeerField: Changes.FieldChange]()
for field in PeerField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""):
break
case ("", _):
changes[field] = .added
case (_, ""):
changes[field] = .removed
case (let this, let other):
if this != other {
changes[field] = .modified(newValue: other)
}
}
}
scratchpad = otherScratchPad
return changes
}
}
enum SaveResult<Configuration> {
case saved(Configuration)
case error(String) // TODO: Localize error messages
case error(String)
}
var interfaceData: InterfaceData
var peersData: [PeerData]
private(set) var interfaceData: InterfaceData
private(set) 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 +510,47 @@ 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 updateDNSServersInAllowedIPsIfRequired(oldDNSServers: String, newDNSServers: String) -> Bool {
guard peersData.count == 1, let firstPeer = peersData.first else { return false }
guard firstPeer.shouldAllowExcludePrivateIPsControl && firstPeer.excludePrivateIPsValue else { return false }
let allowedIPStrings = firstPeer[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let oldDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(oldDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
let newDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(newDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
let updatedAllowedIPStrings = allowedIPStrings.filter { !oldDNSServerStrings.contains($0) } + newDNSServerStrings
firstPeer[.allowedIPs] = updatedAllowedIPStrings.joined(separator: ", ")
return true
}
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,46 +560,138 @@ 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) {
case .none:
return "Off"
case .useOnDemandOverWiFiOrCellular:
return "Wi-Fi or cellular"
case .useOnDemandOverWiFiOnly:
return "Wi-Fi only"
case .useOnDemandOverCellularOnly:
return "Cellular only"
func asWgQuickConfig() -> String? {
let saveResult = save()
if case .saved(let tunnelConfiguration) = saveResult {
return tunnelConfiguration.asWgQuickConfig()
}
return nil
}
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting {
if (activateOnDemandSetting.isActivateOnDemandEnabled) {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
@discardableResult
func applyConfiguration(other: TunnelConfiguration) -> Changes {
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
for otherPeer in other.peers {
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
let peerData = peersData[peersDataIndex]
let changes = peerData.applyConfiguration(other: otherPeer)
if !changes.isEmpty {
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
}
}
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
}
}
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
return .useOnDemandOverWiFiOrCellular
}
var removedPeerIndices = [Int]()
for (index, peerData) in peersData.enumerated().reversed() {
if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
removedPeerIndices.append(index)
peersData.remove(at: index)
}
}
var addedPeerIndices = [Int]()
for otherPeer in other.peers {
if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
addedPeerIndices.append(peersData.count)
let peerData = PeerData(index: peersData.count)
peerData.validatedConfiguration = otherPeer
peersData.append(peerData)
}
}
for (index, peer) in peersData.enumerated() {
peer.index = index
peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState()
}
return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
}
}
private func prettyBytes(_ bytes: UInt64) -> String {
switch bytes {
case 0..<1024:
return "\(bytes) B"
case 1024 ..< (1024 * 1024):
return String(format: "%.2f", Double(bytes) / 1024) + " KiB"
case 1024 ..< (1024 * 1024 * 1024):
return String(format: "%.2f", Double(bytes) / (1024 * 1024)) + " MiB"
case 1024 ..< (1024 * 1024 * 1024 * 1024):
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024)) + " GiB"
default:
return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024 * 1024)) + " TiB"
}
}
private func prettyTimeAgo(timestamp: Date) -> String {
let now = Date()
let timeInterval = Int64(now.timeIntervalSince(timestamp))
switch timeInterval {
case ..<0: return tr("tunnelHandshakeTimestampSystemClockBackward")
case 0: return tr("tunnelHandshakeTimestampNow")
default:
return tr(format: "tunnelHandshakeTimestampAgo (%@)", prettyTime(secondsLeft: timeInterval))
}
}
private func prettyTime(secondsLeft: Int64) -> String {
var left = secondsLeft
var timeStrings = [String]()
let years = left / (365 * 24 * 60 * 60)
left = left % (365 * 24 * 60 * 60)
let days = left / (24 * 60 * 60)
left = left % (24 * 60 * 60)
let hours = left / (60 * 60)
left = left % (60 * 60)
let minutes = left / 60
let seconds = left % 60
#if os(iOS)
if years > 0 {
return years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years)
}
if days > 0 {
return days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days)
}
if hours > 0 {
let hhmmss = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
return tr(format: "tunnelHandshakeTimestampHours hh:mm:ss (%@)", hhmmss)
}
if minutes > 0 {
let mmss = String(format: "%02d:%02d", minutes, seconds)
return tr(format: "tunnelHandshakeTimestampMinutes mm:ss (%@)", mmss)
}
return seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds)
#elseif os(macOS)
if years > 0 {
timeStrings.append(years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years))
}
if days > 0 {
timeStrings.append(days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days))
}
if hours > 0 {
timeStrings.append(hours == 1 ? tr(format: "tunnelHandshakeTimestampHour (%d)", hours) : tr(format: "tunnelHandshakeTimestampHours (%d)", hours))
}
if minutes > 0 {
timeStrings.append(minutes == 1 ? tr(format: "tunnelHandshakeTimestampMinute (%d)", minutes) : tr(format: "tunnelHandshakeTimestampMinutes (%d)", minutes))
}
if seconds > 0 {
timeStrings.append(seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds))
}
return timeStrings.joined(separator: ", ")
#endif
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
@ -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(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = UIColor.white
window.backgroundColor = .white
self.window = window
let mainVC = MainViewController()
@ -27,8 +27,10 @@ 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)
guard let tunnelsManager = mainVC?.tunnelsManager else { return true }
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
_ = FileManager.deleteFile(at: url)
}
return true
}
@ -37,8 +39,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
// MARK: State restoration
extension AppDelegate {
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
@ -48,15 +48,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 +60,6 @@ extension AppDelegate {
// Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false)
}
}
return nil
}

View File

@ -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>

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class ConfirmationAlertPresenter {
static func showConfirmationAlert(message: String, buttonTitle: String, from sourceObject: AnyObject, presentingVC: UIViewController, 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)
if let sourceView = sourceObject as? UIView {
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
} else if let sourceBarButtonItem = sourceObject as? UIBarButtonItem {
alert.popoverPresentationController?.barButtonItem = sourceBarButtonItem
}
presentingVC.present(alert, animated: true, completion: nil)
}
}

View File

@ -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
}
}

View File

@ -1,15 +1,14 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
import os.log
class ErrorPresenter {
static func showErrorAlert(error: WireGuardAppError, from sourceVC: UIViewController?,
onDismissal: (() -> Void)? = nil, onPresented: (() -> Void)? = nil) {
guard let sourceVC = sourceVC else { return }
let (title, message) = error.alertText()
let okAction = UIAlertAction(title: "OK", style: .default) { (_) in
class ErrorPresenter: ErrorPresenterProtocol {
static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) {
guard let sourceVC = sourceVC as? UIViewController else { return }
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
onDismissal?()
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
@ -17,15 +16,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)
}
}

View File

@ -77,7 +77,7 @@
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Camera is used for scanning QR codes for importing WireGuard configurations</string>
<string>Localized</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@ -122,7 +122,9 @@
</dict>
</dict>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Localized</string>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID)</string>
<string>group.$(APP_ID_IOS)</string>
</dict>
</plist>

View File

@ -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")
}
}

View File

@ -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
}
}

View File

@ -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 = ""
}
}

View File

@ -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
}
}

View File

@ -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")
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class ChevronCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel?.text = value }
}
var detailMessage: String {
get { return detailTextLabel?.text ?? "" }
set(value) { detailTextLabel?.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
accessoryType = .disclosureIndicator
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
}
}

View File

@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class EditableTextCell: UITableViewCell {
var message: String {
get { return valueTextField.text ?? "" }
set(value) { valueTextField.text = value }
}
let valueTextField: UITextField = {
let valueTextField = UITextField()
valueTextField.textAlignment = .left
valueTextField.isEnabled = true
valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
valueTextField.adjustsFontForContentSizeCategory = true
valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no
return valueTextField
}()
var onValueBeingEdited: ((String) -> Void)?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
valueTextField.delegate = self
contentView.addSubview(valueTextField)
valueTextField.translatesAutoresizingMaskIntoConstraints = false
let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1)
bottomAnchorConstraint.priority = .defaultLow
NSLayoutConstraint.activate([
valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
bottomAnchorConstraint
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func beginEditing() {
valueTextField.becomeFirstResponder()
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
}
}
extension EditableTextCell: UITextFieldDelegate {
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
}
}

View File

@ -0,0 +1,220 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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, String) -> Void)?
var onValueBeingEdited: ((String) -> Void)?
var observationToken: AnyObject?
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
observationToken = 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?(textFieldValueOnBeginEditing, 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
}
}

View File

@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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)?
var observationToken: AnyObject?
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
observationToken = nil
}
}

View File

@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class TextCell: UITableViewCell {
var message: String {
get { return textLabel?.text ?? "" }
set(value) { textLabel!.text = value }
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setTextColor(_ color: UIColor) {
textLabel?.textColor = color
}
func setTextAlignment(_ alignment: NSTextAlignment) {
textLabel?.textAlignment = alignment
}
override func prepareForReuse() {
super.prepareForReuse()
message = ""
setTextColor(.black)
setTextAlignment(.left)
}
}

View File

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

View File

@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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")
}
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
statusSwitch.isEnabled = !editing
}
private func reset() {
statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false
busyIndicator.stopAnimating()
}
override func prepareForReuse() {
super.prepareForReuse()
reset()
}
}

View File

@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
@ -7,25 +7,23 @@ class MainViewController: UISplitViewController {
var tunnelsManager: TunnelsManager?
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController?
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 +33,49 @@ 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
s.onTunnelsManagerReady?(tunnelsManager)
s.onTunnelsManagerReady = nil
self.onTunnelsManagerReady?(tunnelsManager)
self.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 +86,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 {

View File

@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
// Copyright © 2018-2019 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
}

View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class SSIDOptionDetailTableViewController: UITableViewController {
let selectedSSIDs: [String]
init(title: String, ssids: [String]) {
selectedSSIDs = ssids
super.init(style: .grouped)
self.title = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(TextCell.self)
}
}
extension SSIDOptionDetailTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return selectedSSIDs.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = selectedSSIDs[indexPath.row]
return cell
}
}

View File

@ -0,0 +1,295 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
import SystemConfiguration.CaptiveNetwork
protocol SSIDOptionEditTableViewControllerDelegate: class {
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
}
class SSIDOptionEditTableViewController: UITableViewController {
private enum Section {
case ssidOption
case selectedSSIDs
case addSSIDs
}
private enum AddSSIDRow {
case addConnectedSSID(connectedSSID: String)
case addNewSSID
}
weak var delegate: SSIDOptionEditTableViewControllerDelegate?
private var sections = [Section]()
private var addSSIDRows = [AddSSIDRow]()
let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
.anySSID,
.onlySpecificSSIDs,
.exceptSpecificSSIDs
]
var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
var selectedSSIDs: [String]
var connectedSSID: String?
init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
selectedOption = option
selectedSSIDs = ssids
super.init(style: .grouped)
connectedSSID = getConnectedSSID()
loadSections()
loadAddSSIDRows()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
title = tr("tunnelOnDemandSSIDViewTitle")
tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension
tableView.register(CheckmarkCell.self)
tableView.register(EditableTextCell.self)
tableView.register(TextCell.self)
tableView.isEditing = true
tableView.allowsSelectionDuringEditing = true
}
func loadSections() {
sections.removeAll()
sections.append(.ssidOption)
if selectedOption != .anySSID {
sections.append(.selectedSSIDs)
sections.append(.addSSIDs)
}
}
func loadAddSSIDRows() {
addSSIDRows.removeAll()
if let connectedSSID = connectedSSID {
if !selectedSSIDs.contains(connectedSSID) {
addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
}
}
addSSIDRows.append(.addNewSSID)
}
func updateTableViewAddSSIDRows() {
guard let addSSIDSection = sections.firstIndex(of: .addSSIDs) else { return }
let numberOfAddSSIDRows = addSSIDRows.count
let numberOfAddSSIDRowsInTableView = tableView.numberOfRows(inSection: addSSIDSection)
switch (numberOfAddSSIDRowsInTableView, numberOfAddSSIDRows) {
case (1, 2):
tableView.insertRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
case (2, 1):
tableView.deleteRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
default:
break
}
}
override func viewWillDisappear(_ animated: Bool) {
delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs)
}
}
extension SSIDOptionEditTableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch sections[section] {
case .ssidOption:
return ssidOptionFields.count
case .selectedSSIDs:
return selectedSSIDs.isEmpty ? 1 : selectedSSIDs.count
case .addSSIDs:
return addSSIDRows.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch sections[indexPath.section] {
case .ssidOption:
return ssidOptionCell(for: tableView, at: indexPath)
case .selectedSSIDs:
if !selectedSSIDs.isEmpty {
return selectedSSIDCell(for: tableView, at: indexPath)
} else {
return noSSIDsCell(for: tableView, at: indexPath)
}
case .addSSIDs:
return addSSIDCell(for: tableView, at: indexPath)
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
switch sections[indexPath.section] {
case .ssidOption:
return false
case .selectedSSIDs:
return !selectedSSIDs.isEmpty
case .addSSIDs:
return true
}
}
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
switch sections[indexPath.section] {
case .ssidOption:
return .none
case .selectedSSIDs:
return .delete
case .addSSIDs:
return .insert
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch sections[section] {
case .ssidOption:
return nil
case .selectedSSIDs:
return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
case .addSSIDs:
return tr("tunnelOnDemandSectionTitleAddSSIDs")
}
}
private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = ssidOptionFields[indexPath.row]
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.localizedUIString
cell.isChecked = selectedOption == field
cell.isEditing = false
return cell
}
private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = tr("tunnelOnDemandNoSSIDs")
cell.setTextColor(.gray)
cell.setTextAlignment(.center)
return cell
}
private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = selectedSSIDs[indexPath.row]
cell.isEditing = true
cell.onValueBeingEdited = { [weak self, weak cell] text in
guard let self = self, let cell = cell else { return }
if let row = self.tableView.indexPath(for: cell)?.row {
self.selectedSSIDs[row] = text
self.loadAddSSIDRows()
self.updateTableViewAddSSIDRows()
}
}
return cell
}
private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
switch addSSIDRows[indexPath.row] {
case .addConnectedSSID:
cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID!)
case .addNewSSID:
cell.message = tr("tunnelOnDemandAddMessageAddNewSSID")
}
cell.isEditing = true
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch sections[indexPath.section] {
case .ssidOption:
assertionFailure()
case .selectedSSIDs:
assert(editingStyle == .delete)
selectedSSIDs.remove(at: indexPath.row)
if !selectedSSIDs.isEmpty {
tableView.deleteRows(at: [indexPath], with: .automatic)
} else {
tableView.reloadRows(at: [indexPath], with: .automatic)
}
loadAddSSIDRows()
updateTableViewAddSSIDRows()
case .addSSIDs:
assert(editingStyle == .insert)
let newSSID: String
switch addSSIDRows[indexPath.row] {
case .addConnectedSSID(let connectedSSID):
newSSID = connectedSSID
case .addNewSSID:
newSSID = ""
}
selectedSSIDs.append(newSSID)
loadSections()
let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)!
let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection)
if selectedSSIDs.count == 1 {
tableView.reloadRows(at: [indexPath], with: .automatic)
} else {
tableView.insertRows(at: [indexPath], with: .automatic)
}
loadAddSSIDRows()
updateTableViewAddSSIDRows()
if newSSID.isEmpty {
if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
selectedSSIDCell.beginEditing()
}
}
}
}
}
extension SSIDOptionEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch sections[indexPath.section] {
case .ssidOption:
return indexPath
case .selectedSSIDs, .addSSIDs:
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch sections[indexPath.section] {
case .ssidOption:
let previousOption = selectedOption
selectedOption = ssidOptionFields[indexPath.row]
loadSections()
if previousOption == .anySSID {
let indexSet = IndexSet(1 ... 2)
tableView.insertSections(indexSet, with: .fade)
}
if selectedOption == .anySSID {
let indexSet = IndexSet(1 ... 2)
tableView.deleteSections(indexSet, with: .fade)
}
tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none)
case .selectedSSIDs, .addSSIDs:
assertionFailure()
}
}
}
private func getConnectedSSID() -> String? {
guard let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] else { return nil }
for interface in supportedInterfaces {
if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
return !ssid.isEmpty ? ssid : nil
}
}
}
return nil
}

View File

@ -0,0 +1,204 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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) {
PrivateDataConfirmation.confirmAccess(to: tr("iosExportPrivateData")) { [weak self] in
guard let self = self else { return }
guard let tunnelsManager = self.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
}
}
let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
DispatchQueue.main.async {
guard isWritten else {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
return
}
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
}
}
}

View File

@ -0,0 +1,457 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class TunnelDetailTableViewController: UITableViewController {
private enum Section {
case status
case interface
case peer(index: Int, peer: TunnelViewModel.PeerData)
case onDemand
case delete
}
static let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses,
.listenPort, .mtu, .dns
]
static let peerFields: [TunnelViewModel.PeerField] = [
.publicKey, .preSharedKey, .endpoint,
.allowedIPs, .persistentKeepAlive,
.rxBytes, .txBytes, .lastHandshakeTime
]
static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
.onDemand, .ssid
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel
var onDemandViewModel: ActivateOnDemandViewModel
private var sections = [Section]()
private var interfaceFieldIsVisible = [Bool]()
private var peerFieldIsVisible = [[Bool]]()
private var statusObservationToken: AnyObject?
private var onDemandObservationToken: AnyObject?
private var reloadRuntimeConfigurationTimer: Timer?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager
self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
super.init(style: .grouped)
loadSections()
loadVisibleFields()
statusObservationToken = tunnel.observe(\.status) { [weak self] _, _ in
guard let self = self else { return }
if tunnel.status == .active {
self.startUpdatingRuntimeConfiguration()
} else if tunnel.status == .inactive {
self.reloadRuntimeConfiguration()
self.stopUpdatingRuntimeConfiguration()
}
}
onDemandObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
// Handle On-Demand getting turned on/off outside of the app
self?.onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
self?.updateActivateOnDemandFields()
}
}
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.register(SwitchCell.self)
tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self)
tableView.register(ChevronCell.self)
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
}
private func loadSections() {
sections.removeAll()
sections.append(.status)
sections.append(.interface)
for (index, peer) in tunnelViewModel.peersData.enumerated() {
sections.append(.peer(index: index, peer: peer))
}
sections.append(.onDemand)
sections.append(.delete)
}
private func loadVisibleFields() {
let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields)
interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) }
peerFieldIsVisible = tunnelViewModel.peersData.map { peer in
let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields)
return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) }
}
}
override func viewWillAppear(_ animated: Bool) {
if tunnel.status == .active {
self.startUpdatingRuntimeConfiguration()
}
}
override func viewDidDisappear(_ animated: Bool) {
stopUpdatingRuntimeConfiguration()
}
@objc func editTapped() {
PrivateDataConfirmation.confirmAccess(to: tr("iosViewPrivateData")) { [weak self] in
guard let self = self else { return }
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet
self.present(editNC, animated: true)
}
}
func startUpdatingRuntimeConfiguration() {
reloadRuntimeConfiguration()
reloadRuntimeConfigurationTimer?.invalidate()
let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
self?.reloadRuntimeConfiguration()
}
reloadRuntimeConfigurationTimer = reloadTimer
RunLoop.main.add(reloadTimer, forMode: .common)
}
func stopUpdatingRuntimeConfiguration() {
reloadRuntimeConfigurationTimer?.invalidate()
reloadRuntimeConfigurationTimer = nil
}
func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
// Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
guard let tableView = self.tableView else { return }
let sections = self.sections
let interfaceSectionIndex = sections.firstIndex {
if case .interface = $0 {
return true
} else {
return false
}
}!
let firstPeerSectionIndex = interfaceSectionIndex + 1
var interfaceFieldIsVisible = self.interfaceFieldIsVisible
var peerFieldIsVisible = self.peerFieldIsVisible
func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue }
if case .modified(let newValue) = change {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
let indexPath = IndexPath(row: row, section: section)
if let cell = tableView.cellForRow(at: indexPath) as? KeyValueCell {
cell.value = newValue
}
}
}
}
func handleSectionRowsInsertedOrRemoved<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
var fieldIsVisible = fieldIsVisibleInput
var removedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
removedIndexPaths.append(IndexPath(row: row, section: section))
fieldIsVisible[index] = false
}
if !removedIndexPaths.isEmpty {
tableView.deleteRows(at: removedIndexPaths, with: .automatic)
}
var addedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated() where changes[field] == .added {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
addedIndexPaths.append(IndexPath(row: row, section: section))
fieldIsVisible[index] = true
}
if !addedIndexPaths.isEmpty {
tableView.insertRows(at: addedIndexPaths, with: .automatic)
}
}
let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
if !changes.interfaceChanges.isEmpty {
handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
section: interfaceSectionIndex, changes: changes.interfaceChanges)
}
for (peerIndex, peerChanges) in changes.peerChanges {
handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
}
let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
let peersRemovedSectionIndices = changes.peersRemovedIndices.map { firstPeerSectionIndex + $0 }
let peersInsertedSectionIndices = changes.peersInsertedIndices.map { firstPeerSectionIndex + $0 }
if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !peersRemovedSectionIndices.isEmpty || !peersInsertedSectionIndices.isEmpty {
tableView.beginUpdates()
if isAnyInterfaceFieldAddedOrRemoved {
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, section: interfaceSectionIndex, changes: changes.interfaceChanges)
}
if isAnyPeerFieldAddedOrRemoved {
for (peerIndex, peerChanges) in changes.peerChanges {
handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
}
}
if !peersRemovedSectionIndices.isEmpty {
tableView.deleteSections(IndexSet(peersRemovedSectionIndices), with: .automatic)
}
if !peersInsertedSectionIndices.isEmpty {
tableView.insertSections(IndexSet(peersInsertedSectionIndices), with: .automatic)
}
self.loadSections()
self.loadVisibleFields()
tableView.endUpdates()
} else {
self.loadSections()
self.loadVisibleFields()
}
}
private func reloadRuntimeConfiguration() {
tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
guard let tunnelConfiguration = tunnelConfiguration else { return }
guard let self = self else { return }
self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
}
}
private func updateActivateOnDemandFields() {
guard let onDemandSection = sections.firstIndex(where: { if case .onDemand = $0 { return true } else { return false } }) else { return }
let numberOfTableViewOnDemandRows = tableView.numberOfRows(inSection: onDemandSection)
let ssidRowIndexPath = IndexPath(row: 1, section: onDemandSection)
switch (numberOfTableViewOnDemandRows, onDemandViewModel.isWiFiInterfaceEnabled) {
case (1, true):
tableView.insertRows(at: [ssidRowIndexPath], with: .automatic)
case (2, false):
tableView.deleteRows(at: [ssidRowIndexPath], with: .automatic)
default:
break
}
tableView.reloadSections(IndexSet(integer: onDemandSection), with: .automatic)
}
}
extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
loadSections()
loadVisibleFields()
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 interfaceFieldIsVisible.filter { $0 }.count
case .peer(let peerIndex, _):
return peerFieldIsVisible[peerIndex].filter { $0 }.count
case .onDemand:
return onDemandViewModel.isWiFiInterfaceEnabled ? 2 : 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 index, let peer):
return peerCell(for: tableView, at: indexPath, with: peer, peerIndex: index)
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)
cell.observationToken = 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 visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
let field = visibleInterfaceFields[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, peerIndex: Int) -> UITableViewCell {
let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
let field = visiblePeerFields[indexPath.row]
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString
if field == .persistentKeepAlive {
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
} else if field == .preSharedKey {
cell.value = tr("tunnelPeerPresharedKeyEnabled")
} else {
cell.value = peerData[field]
}
return cell
}
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let field = TunnelDetailTableViewController.onDemandFields[indexPath.row]
if field == .onDemand {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString
cell.value = onDemandViewModel.localizedInterfaceDescription
return cell
} else {
assert(field == .ssid)
if onDemandViewModel.ssidOption == .anySSID {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.key = field.localizedUIString
cell.value = onDemandViewModel.ssidOption.localizedUIString
return cell
} else {
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.localizedUIString
cell.detailMessage = onDemandViewModel.localizedSSIDDescription
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 }
ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"),
buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"),
from: cell, presentingVC: self) { [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
}
}
}
}
return cell
}
}
extension TunnelDetailTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section],
case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
return indexPath
}
return nil
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if case .onDemand = sections[indexPath.section],
case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
let ssidDetailVC = SSIDOptionDetailTableViewController(title: onDemandViewModel.ssidOption.localizedUIString, ssids: onDemandViewModel.selectedSSIDs)
navigationController?.pushViewController(ssidDetailVC, animated: true)
}
tableView.deselectRow(at: indexPath, animated: true)
}
}

View File

@ -0,0 +1,503 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 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 onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
.nonWiFiInterface,
.wiFiInterface,
.ssid
]
let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel
var onDemandViewModel: ActivateOnDemandViewModel
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)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
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)
onDemandViewModel = ActivateOnDemandViewModel()
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(ChevronCell.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):
let onDemandOption = onDemandViewModel.toOnDemandOption()
if let tunnel = tunnel {
// We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [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, onDemandOption: onDemandOption) { [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 onDemandViewModel.isWiFiInterfaceEnabled {
return 3
} else {
return 2
}
}
}
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().base64Key() ?? ""
if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
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: { $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
case .status, .toggleStatus:
fatalError("Unexpected interface field")
}
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
}
cell.onValueChanged = { [weak self] oldValue, newValue in
guard let self = self else { return }
let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
if isAllowedIPsChanged {
let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } }
if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
}
}
}
} 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 }
ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"),
buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"),
from: cell, presentingVC: self) { [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)
//swiftlint:disable:next trailing_closure
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
case .rxBytes, .txBytes, .lastHandshakeTime:
fatalError()
}
cell.isValueValid = !peerData.fieldsWithError.contains(field)
cell.value = peerData[field]
if field == .allowedIPs {
let firstInterfaceSection = sections.firstIndex { $0 == .interface }!
let interfaceSubSection = interfaceFieldsBySection.firstIndex { $0.contains(.dns) }!
let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex { $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 {
let field = onDemandFields[indexPath.row]
if indexPath.row < 2 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.localizedUIString
cell.isOn = onDemandViewModel.isEnabled(field: field)
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }
self.onDemandViewModel.setEnabled(field: field, isEnabled: isOn)
let section = self.sections.firstIndex { $0 == .onDemand }!
let indexPath = IndexPath(row: 2, section: section)
if field == .wiFiInterface {
if isOn {
tableView.insertRows(at: [indexPath], with: .fade)
} else {
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
}
return cell
} else {
let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = field.localizedUIString
cell.detailMessage = onDemandViewModel.localizedSSIDDescription
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)
}
}
extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section], indexPath.row == 2 {
return indexPath
} else {
return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch sections[indexPath.section] {
case .onDemand:
assert(indexPath.row == 2)
tableView.deselectRow(at: indexPath, animated: true)
let ssidOptionVC = SSIDOptionEditTableViewController(option: onDemandViewModel.ssidOption, ssids: onDemandViewModel.selectedSSIDs)
ssidOptionVC.delegate = self
navigationController?.pushViewController(ssidOptionVC, animated: true)
default:
assertionFailure()
}
}
}
extension TunnelEditTableViewController: SSIDOptionEditTableViewControllerDelegate {
func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
onDemandViewModel.selectedSSIDs = ssids
onDemandViewModel.ssidOption = option
onDemandViewModel.fixSSIDOption()
if let onDemandSection = sections.firstIndex(where: { $0 == .onDemand }) {
if let ssidRowIndex = onDemandFields.firstIndex(of: .ssid) {
let indexPath = IndexPath(row: ssidRowIndex, section: onDemandSection)
tableView.reloadRows(at: [indexPath], with: .none)
}
}
}
}

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