Compare commits

...

218 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-20 16:42:27 +05:30
Roopesh Chander 69b33c0fad macOS: Edit view: Save on Cmd+S
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 15:45:26 +05:30
Roopesh Chander 167e4f0bf2 macOS: Edit view: Dismiss on Esc
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 13:23:52 +05:30
Roopesh Chander 6e3b28852a macOS: Log view: Dismiss on Esc
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-05-12 13:21:42 +05:30
Roopesh Chander 5914e868ab iOS: Log view: Improve the look
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-28 14:29:22 +05:30
Jason A. Donenfeld 83ea9d6fa7 wireguard-go-bridge: add missing format specifier for error
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-24 13:24:03 +02:00
Jason A. Donenfeld b954e9a4fd Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-23 06:49:16 +02:00
Roopesh Chander 76894fba68 Xcode: Use dwarf for debug and dwarf-with-dsym for release
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-21 18:22:31 +05:30
Roopesh Chander 89a564ce62 Swift 5 migration: Make use of Result type
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-21 17:51:42 +05:30
Roopesh Chander 178fe86d36 macOS: Detect when updating from the App Store
And show an alert when tunnels are active during updation -- that
might cause the update to not work correctly.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-21 15:43:10 +05:30
Jason A. Donenfeld 571349bb3d Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-12 10:32:06 +02:00
Roopesh Chander 98ebd55208 Log view: Don't use a global array to store log entries
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-10 17:57:36 +05:30
Roopesh Chander 83d0d34411 macOS: Log view: Stop updating the log once the log view is dismissed
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-10 15:42:39 +05:30
Jason A. Donenfeld 8c7c4b6792 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-09 10:49:48 +02:00
Jason A. Donenfeld 5b34f49166 wireguard-go-bridge: bump again for version file placement
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-09 10:43:24 +02:00
Roopesh Chander cef3957875 Swift 5 migration: Handle changes in Data's pointer interface
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-09 11:25:04 +05:30
Roopesh Chander d9e88c51bd Swift 5 migration: Fix switch warnings
We now get a warning when switching over enums from system
frameworks even when we handle all public cases because
there can be future cases that aren't handled.

When such a future case is introduced, we'll get a warning.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-09 11:25:04 +05:30
Jason A. Donenfeld db876647d6 wireguard-go-bridge: version bump to new tag
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-04-09 07:44:50 +02:00
Roopesh Chander 90eb45e287 Xcode: Move to Swift 5.0
No code changes were necessary

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-07 16:42:36 +05:30
Roopesh Chander 9f3d86723a macOS: Minor fix to export panel texts
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-06 17:59:42 +05:30
Roopesh Chander 11063d0f88 macOS: Tunnels list: Suppress alert buttons when removing tunnels is in progress
Also refactor the deletion alert into a separate helper class

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-06 17:53:41 +05:30
Roopesh Chander 557ee4390b TunnelsManager: When setting a config, also set isAvailable cache
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-06 00:26:06 +05:30
Roopesh Chander 9ce42d152d macOS: Tunnels list: Show the confirmation alert till removal completes
Fix tunnel selection during deletion

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-06 00:08:45 +05:30
Roopesh Chander 4c1b2e1258 TunnelsManager: Fix comparing tunnels with tunnelProviders in reload()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-05 13:43:08 +05:30
Roopesh Chander 3bd611aa7c TunnelsManager: Cache isTunnelConfigurationAvailableInKeychain
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-05 13:29:17 +05:30
Roopesh Chander adbe0b065e macOS: Attempt to remove keychain item only if verified
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-04 15:29:25 +05:30
Roopesh Chander 6015661beb macOS: Simplify reusing of the detail VC when applicable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-04 12:24:32 +05:30
Roopesh Chander 6e6a6b88fb macOS: Hide other-user tunnels in the status menu
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-03 19:18:53 +05:30
Roopesh Chander 9690365dd4 macOS: Better handling of tunnels created by another user
Previously, the tunnels just got deleted.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-03 19:04:12 +05:30
Roopesh Chander 0299c3929e iOS: Log view: Make log text selectable
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-01 23:29:15 +05:30
Roopesh Chander 0043233872 macOS: Log view: Fix autoscroll to end of log
Looks like the tableview doesn't know how much to scroll to get to the
end when we use usesAutomaticRowHeights. So we wait for the tableview
to realize its frame has changed and then scroll to the bottom of the
frame explicitly.

Also, we keep track of whether the scroll view is scrolled to the end or
not every time scrolling happens, not just when we add log entries to
the table.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-04-01 23:07:48 +05:30
Roopesh Chander a74dd24578 macOS: Bring app to front before 'exiting with an active tunnel' alert
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-29 00:17:37 +05:30
Roopesh Chander dd9506ecee macOS: If a sheet is being shown, ignore quit and bring window to front
Otherwise, the 'exiting with an active tunnel' alert could get queued up
to be shown after the current sheet is dismissed.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-29 00:17:37 +05:30
Jason A. Donenfeld 0c2eb003a0 wireguard-go-bridge: update deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-28 15:55:53 +01:00
Roopesh Chander f83f159f97 macOS: Log view: No need to disable Close button
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-28 19:32:58 +05:30
Roopesh Chander 6175de0438 iOS: Ability to view the log
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-28 19:28:27 +05:30
Roopesh Chander bd61be52e6 iOS: Xcode: Minor project rearrangement
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-28 14:10:42 +05:30
Roopesh Chander 909f88be70 macOS: Ability to view the log
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-28 13:57:06 +05:30
Roopesh Chander b7c3bd0d8c Add LogViewHelper
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-27 17:55:52 +05:30
Roopesh Chander 4237ab4a6f macOS: Syntax highlighter: Free spans array
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-22 17:53:34 +05:30
Roopesh Chander 0fcaf6debb macOS: Hide exclude private IPs when PrivateKey / PublicKey is missing
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-22 16:00:45 +05:30
Roopesh Chander dbd5ea1ff0 macOS: Syntax highlighter: Swift can bridge c strings automatically
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-22 15:31:02 +05:30
Roopesh Chander 9afe230c10 macOS: On Add new, Exclude Private IPs should remain hidden
because there aren't any peers in the bootstrapped config.

Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-22 15:15:26 +05:30
Roopesh Chander 4bdfbb518e Xcode: iOS: Remove armv7 as 'Required device capabilities'
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-20 14:23:56 +05:30
Jason A. Donenfeld fbe101eabb macOS: Privacy notice is provided by system dialogs
So it really doesn't make sense to add our own. This causes several
popups when trying to add a tunnel, which is madness.

Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-20 04:24:23 +01:00
Jason A. Donenfeld cda3170970 macOS: Login item: Add a simple login item
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 21:15:38 -06:00
Jason A. Donenfeld a5e7c3906b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-19 21:25:38 +01:00
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 7a3f65fd2f macOS: Add 'Deactivate' status menu item
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-19 21:23:46 +01:00
Jason A. Donenfeld 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
Jason A. Donenfeld af9c800af8 Swiftlint: variable_name -> identifier_name
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 22:26:13 -06:00
Jason A. Donenfeld a9b925c69b Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 22:13:27 -06:00
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 0dd22ca45a iOS: Tunnel edit: Add missing enum values
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 14:54:05 -06:00
Roopesh Chander bca9fead5e macOS: ButtonedDetailViewController: Set min dimensions
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-19 01:28:52 +05:30
Jason A. Donenfeld 51822f722a ringlogger: document races
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-18 12:50:00 -06:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 9c8231dcf7 on-demand: macOS: Remove unused class ControlRow
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander f56b2ad968 on-demand: macOS: Remove unused class PopupRow
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:56 +01:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 63299a2752 on-demand: macOS: Tunnel detail: List SSIDs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 5941bf181c on-demand: iOS: Support for SSIDs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
Roopesh Chander 7a450089c0 on-demand: Introducing ActivateOnDemandViewModel
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 9795b0609a macOS: Localize tooltips
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
Roopesh Chander 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
Roopesh Chander f81275812c macOS: Nullify observationToken on prepareForReuse()
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-18 06:46:55 +01:00
Jason A. Donenfeld 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
Jason A. Donenfeld a6f80135ef ringlogger: support mpsc for singlefile
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-17 08:51:27 +01:00
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 3e05da4486 macOS: KeyValueImageRow class
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-17 02:27:46 +05:30
Jason A. Donenfeld c750f28c67 wireguard-go-bridge: update deps
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-12 10:45:14 +01:00
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander db6f0729c6 macOS: Generalize NoTunnelsDetailVC into a ButtonedDetailVC
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-10 19:22:33 +05:30
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 202e7a4890 Importing: Simplify TunnelImporter
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-03-04 14:13:49 +05:30
Jason A. Donenfeld d7b16ffb1f wireguard-go-bridge: use go modules
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-03-03 06:28:07 +01:00
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander ea5996abe0 macOS: Tunnel edit: s/populateTextFields()/populateFields()/g;
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-24 15:14:35 +05:30
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
Jason A. Donenfeld c195760b15 macOS: Specify crypto compliance
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-19 16:12:33 +01:00
Roopesh Chander 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
Roopesh Chander 5031a7db4c macOS: Exclude private IPs
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 18:25:17 +05:30
Roopesh Chander 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
Jason A. Donenfeld 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
Roopesh Chander 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
Roopesh Chander 2fb9d6af71 ConfTextStorage: Make fieldType an enum
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-16 00:26:49 +05:30
Jason A. Donenfeld 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
Roopesh Chander 910fdfc321 macOS: Tunnel detail: Set min width/height
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-15 15:44:11 +05:30
Roopesh Chander 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
Roopesh Chander cf51344444 .mobileconfig: fix lists
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-15 14:05:15 +05:30
Jason A. Donenfeld fda36b571d README: supports macOS
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:39:34 +01:00
Jason A. Donenfeld 1be3224d7a README: recursive cloning
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:37:02 +01:00
Jason A. Donenfeld ca088e9ddc README: Xcode has a lowercase 'c'
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-15 02:35:10 +01:00
Jason A. Donenfeld 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
Jason A. Donenfeld 58181a4d40 Version bump
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 23:07:06 +01:00
Jason A. Donenfeld cf816ede13 wireguard-go: bump for ARM64 ChaCha20
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-14 12:00:59 +01:00
Jason A. Donenfeld fbac282bdc .mobileconfig: fix formatting
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:31:02 +01:00
Jason A. Donenfeld 61f5e017c8 .mobileconfig: note keychain limitation
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
2019-02-13 14:04:54 +01:00
Roopesh Chander 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
Roopesh Chander 5792db22a6 Log migration of tunnel configuration
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:17:32 +05:30
Roopesh Chander 9d5aa1d8fa Document installing WireGuard tunnels using Configuration Profiles
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-12 19:14:53 +05:30
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander b5b72b309f Info.plist: Localize with InfoPlist.strings
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-11 16:08:55 +05:30
Roopesh Chander 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
Roopesh Chander 115059f2bb macOS: Adapt to the new applyConfiguration API
Signed-off-by: Roopesh Chander <roop@roopc.net>
2019-02-10 03:35:24 +05:30
Roopesh Chander 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
Roopesh Chander 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
Roopesh Chander 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
101 changed files with 4717 additions and 1420 deletions
-3
View File
@@ -1,3 +0,0 @@
[submodule "wireguard-go"]
path = wireguard-go
url = https://git.zx2c4.com/wireguard-go
+140
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
+9 -14
View File
@@ -1,19 +1,14 @@
# [WireGuard](https://www.wireguard.com/) for iOS # [WireGuard](https://www.wireguard.com/) for iOS and macOS
This project contains an application for iOS and for macOS, as well as many components shared between the two of them. You may toggle between the two platforms by selecting the target from within Xcode.
## Building ## Building
- Clone this repo: - Clone this repo:
``` ```
$ git clone https://git.zx2c4.com/wireguard-ios $ git clone https://git.zx2c4.com/wireguard-apple
$ cd wireguard-ios $ cd wireguard-apple
```
- Init and update submodule:
```
$ git submodule init
$ git submodule update
``` ```
- Rename and populate developer team ID file: - Rename and populate developer team ID file:
@@ -23,19 +18,19 @@ $ cp WireGuard/WireGuard/Config/Developer.xcconfig.template WireGuard/WireGuard/
$ vim WireGuard/WireGuard/Config/Developer.xcconfig $ vim WireGuard/WireGuard/Config/Developer.xcconfig
``` ```
- Install swiftlint: - Install swiftlint and go:
``` ```
$ brew install swiftlint $ brew install swiftlint go
``` ```
- Open project in XCode: - Open project in Xcode:
``` ```
$ open ./WireGuard/WireGuard.xcodeproj $ 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 ## MIT License
+1 -1
View File
@@ -20,6 +20,6 @@ opt_in_rules:
- unneeded_parentheses_in_closure_argument - unneeded_parentheses_in_closure_argument
- unused_import - unused_import
- trailing_closure - trailing_closure
variable_name: identifier_name:
min_length: min_length:
warning: 0 warning: 0
+1 -9
View File
@@ -27,7 +27,7 @@ extension FileManager {
return sharedFolderURL return sharedFolderURL
} }
static var networkExtensionLogFileURL: URL? { static var logFileURL: URL? {
return sharedFolderURL?.appendingPathComponent("tunnel-log.bin") return sharedFolderURL?.appendingPathComponent("tunnel-log.bin")
} }
@@ -35,14 +35,6 @@ extension FileManager {
return sharedFolderURL?.appendingPathComponent("last-error.txt") return sharedFolderURL?.appendingPathComponent("last-error.txt")
} }
static var appLogFileURL: URL? {
guard let documentDirURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
wg_log(.error, message: "Cannot obtain app documents folder URL")
return nil
}
return documentDirURL.appendingPathComponent("app-log.bin")
}
static func deleteFile(at url: URL) -> Bool { static func deleteFile(at url: URL) -> Bool {
do { do {
try FileManager.default.removeItem(at: url) try FileManager.default.removeItem(at: url)
+1 -1
View File
@@ -112,6 +112,6 @@ class Keychain {
static func verifyReference(called ref: Data) -> Bool { static func verifyReference(called ref: Data) -> Bool {
return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword, return SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
kSecValuePersistentRef as String: ref] as CFDictionary, kSecValuePersistentRef as String: ref] as CFDictionary,
nil) == errSecSuccess nil) != errSecItemNotFound
} }
} }
+8 -10
View File
@@ -12,10 +12,12 @@ public class Logger {
static var global: Logger? static var global: Logger?
var log: OpaquePointer var log: OpaquePointer
var tag: String
init(withFilePath filePath: String) throws { init(tagged tag: String, withFilePath filePath: String) throws {
guard let log = open_log(filePath) else { throw LoggerError.openFailure } guard let log = open_log(filePath) else { throw LoggerError.openFailure }
self.log = log self.log = log
self.tag = tag
} }
deinit { deinit {
@@ -23,17 +25,14 @@ public class Logger {
} }
func log(message: String) { func log(message: String) {
write_msg_to_log(log, message.trimmingCharacters(in: .newlines)) write_msg_to_log(log, tag, message.trimmingCharacters(in: .newlines))
} }
func writeLog(called ourTag: String, mergedWith otherLogFile: String, called otherTag: String, to targetFile: String) -> Bool { func writeLog(to targetFile: String) -> Bool {
guard let other = open_log(otherLogFile) else { return false } return write_log_to_file(targetFile, self.log) == 0
let ret = write_logs_to_file(targetFile, log, ourTag, other, otherTag)
close_log(other)
return ret == 0
} }
static func configureGlobal(withFilePath filePath: String?) { static func configureGlobal(tagged tag: String, withFilePath filePath: String?) {
if Logger.global != nil { if Logger.global != nil {
return return
} }
@@ -41,7 +40,7 @@ public class Logger {
os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error) os_log("Unable to determine log destination path. Log will not be saved to file.", log: OSLog.default, type: .error)
return return
} }
guard let logger = try? Logger(withFilePath: filePath) else { 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) os_log("Unable to open log file for writing. Log will not be saved to file.", log: OSLog.default, type: .error)
return return
} }
@@ -52,7 +51,6 @@ public class Logger {
} }
let goBackendVersion = WIREGUARD_GO_VERSION let goBackendVersion = WIREGUARD_GO_VERSION
Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)") Logger.global?.log(message: "App version: \(appVersion); Go backend version: \(goBackendVersion)")
} }
} }
+79 -53
View File
@@ -6,6 +6,8 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h>
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <time.h> #include <time.h>
#include <errno.h> #include <errno.h>
@@ -19,100 +21,124 @@
enum { enum {
MAX_LOG_LINE_LENGTH = 512, MAX_LOG_LINE_LENGTH = 512,
MAX_LINES = 1024, MAX_LINES = 2048,
MAGIC = 0xbeefbabeU MAGIC = 0xabadbeefU
}; };
struct log_line { struct log_line {
struct timeval tv; atomic_uint_fast64_t time_ns;
char line[MAX_LOG_LINE_LENGTH]; char line[MAX_LOG_LINE_LENGTH];
}; };
struct log { struct log {
struct { uint32_t first, len; } header; atomic_uint_fast32_t next_index;
struct log_line lines[MAX_LINES]; struct log_line lines[MAX_LINES];
uint32_t magic; uint32_t magic;
}; };
void write_msg_to_log(struct log *log, const char *msg) void write_msg_to_log(struct log *log, const char *tag, const char *msg)
{ {
struct log_line *line = &log->lines[(log->header.first + log->header.len) % MAX_LINES]; uint32_t index;
struct log_line *line;
struct timespec ts;
if (log->header.len == MAX_LINES) // Race: This isn't synchronized with the fetch_add below, so items might be slightly out of order.
log->header.first = (log->header.first + 1) % MAX_LINES; clock_gettime(CLOCK_REALTIME, &ts);
else
++log->header.len;
gettimeofday(&line->tv, NULL); // Race: More than MAX_LINES writers and this will clash.
strncpy(line->line, msg, MAX_LOG_LINE_LENGTH - 1); index = atomic_fetch_add(&log->next_index, 1);
line->line[MAX_LOG_LINE_LENGTH - 1] = '\0'; line = &log->lines[index % MAX_LINES];
msync(&log->header, sizeof(log->header), MS_ASYNC); // 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); msync(line, sizeof(*line), MS_ASYNC);
} }
static bool first_before_second(const struct log_line *line1, const struct log_line *line2) int write_log_to_file(const char *file_name, const struct log *input_log)
{ {
if (line1->tv.tv_sec <= line2->tv.tv_sec) struct log *log;
return true; uint32_t l, i;
if (line1->tv.tv_sec == line2->tv.tv_sec)
return line1->tv.tv_usec <= line2->tv.tv_usec;
return false;
}
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2)
{
uint32_t i1, i2, len1 = log1->header.len, len2 = log2->header.len;
FILE *file; FILE *file;
int ret; int ret;
if (len1 > MAX_LINES) log = malloc(sizeof(*log));
len1 = MAX_LINES; if (!log)
if (len2 > MAX_LINES) return -errno;
len2 = MAX_LINES; memcpy(log, input_log, sizeof(*log));
file = fopen(file_name, "w"); file = fopen(file_name, "w");
if (!file) if (!file) {
free(log);
return -errno; return -errno;
}
for (i1 = 0, i2 = 0;;) { 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; struct tm tm;
char buf[MAX_LOG_LINE_LENGTH];
const struct log_line *line1 = &log1->lines[(log1->header.first + i1) % MAX_LINES];
const struct log_line *line2 = &log2->lines[(log2->header.first + i2) % MAX_LINES];
const struct log_line *line;
const char *tag;
if (i1 < len1 && (i2 >= len2 || first_before_second(line1, line2))) { if (!line->time_ns)
line = line1; continue;
tag = tag1;
++i1;
} else if (i2 < len2 && (i1 >= len1 || first_before_second(line2, line1))) {
line = line2;
tag = tag2;
++i2;
} else {
break;
}
memcpy(buf, line->line, MAX_LOG_LINE_LENGTH); if (!localtime_r(&seconds, &tm))
buf[MAX_LOG_LINE_LENGTH - 1] = '\0';
if (!localtime_r(&line->tv.tv_sec, &tm))
goto err; goto err;
if (fprintf(file, "%04d-%02d-%02d %02d:%02d:%02d.%06d: [%s] %s\n",
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_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, line->tv.tv_usec, tm.tm_hour, tm.tm_min, tm.tm_sec, useconds,
tag, buf) < 0) line->line) < 0)
goto err; goto err;
} }
errno = 0; errno = 0;
err: err:
ret = -errno; ret = -errno;
fclose(file); fclose(file);
free(log);
return ret; return ret;
} }
uint32_t view_lines_from_cursor(const struct log *input_log, uint32_t cursor, void *ctx, void(*cb)(const char *, uint64_t, void *))
{
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, ctx);
cursor = (i + 1) % MAX_LINES;
}
free(log);
return cursor;
}
struct log *open_log(const char *file_name) struct log *open_log(const char *file_name)
{ {
int fd; int fd;
+5 -2
View File
@@ -6,9 +6,12 @@
#ifndef RINGLOGGER_H #ifndef RINGLOGGER_H
#define RINGLOGGER_H #define RINGLOGGER_H
#include <stdint.h>
struct log; struct log;
void write_msg_to_log(struct log *log, const char *msg); void write_msg_to_log(struct log *log, const char *tag, const char *msg);
int write_logs_to_file(const char *file_name, const struct log *log1, const char *tag1, const struct log *log2, const char *tag2); 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 *ctx, void(*)(const char *, uint64_t, void *));
struct log *open_log(const char *file_name); struct log *open_log(const char *file_name);
void close_log(struct log *log); void close_log(struct log *log);
@@ -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;
}
+32 -6
View File
@@ -13,8 +13,8 @@ extension Data {
return nil return nil
} }
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_HEX)) var out = Data(repeating: 0, count: Int(WG_KEY_LEN_HEX))
out.withUnsafeMutableBytes { outBytes in out.withUnsafeMutableInt8Bytes { outBytes in
self.withUnsafeBytes { inBytes in self.withUnsafeUInt8Bytes { inBytes in
key_to_hex(outBytes, inBytes) key_to_hex(outBytes, inBytes)
} }
} }
@@ -25,7 +25,7 @@ extension Data {
init?(hexKey hexString: String) { init?(hexKey hexString: String) {
self.init(repeating: 0, count: Int(WG_KEY_LEN)) self.init(repeating: 0, count: Int(WG_KEY_LEN))
if !self.withUnsafeMutableBytes { key_from_hex($0, hexString) } { if !self.withUnsafeMutableUInt8Bytes { key_from_hex($0, hexString) } {
return nil return nil
} }
} }
@@ -35,8 +35,8 @@ extension Data {
return nil return nil
} }
var out = Data(repeating: 0, count: Int(WG_KEY_LEN_BASE64)) var out = Data(repeating: 0, count: Int(WG_KEY_LEN_BASE64))
out.withUnsafeMutableBytes { outBytes in out.withUnsafeMutableInt8Bytes { outBytes in
self.withUnsafeBytes { inBytes in self.withUnsafeUInt8Bytes { inBytes in
key_to_base64(outBytes, inBytes) key_to_base64(outBytes, inBytes)
} }
} }
@@ -47,8 +47,34 @@ extension Data {
init?(base64Key base64String: String) { init?(base64Key base64String: String) {
self.init(repeating: 0, count: Int(WG_KEY_LEN)) self.init(repeating: 0, count: Int(WG_KEY_LEN))
if !self.withUnsafeMutableBytes { key_from_base64($0, base64String) } { if !self.withUnsafeMutableUInt8Bytes { key_from_base64($0, base64String) } {
return nil return nil
} }
} }
} }
extension Data {
func withUnsafeUInt8Bytes<R>(_ body: (UnsafePointer<UInt8>) -> R) -> R {
assert(!isEmpty)
return self.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) -> R in
let bytes = ptr.bindMemory(to: UInt8.self)
return body(bytes.baseAddress!) // might crash if self.count == 0
}
}
mutating func withUnsafeMutableUInt8Bytes<R>(_ body: (UnsafeMutablePointer<UInt8>) -> R) -> R {
assert(!isEmpty)
return self.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) -> R in
let bytes = ptr.bindMemory(to: UInt8.self)
return body(bytes.baseAddress!) // might crash if self.count == 0
}
}
mutating func withUnsafeMutableInt8Bytes<R>(_ body: (UnsafeMutablePointer<Int8>) -> R) -> R {
assert(!isEmpty)
return self.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) -> R in
let bytes = ptr.bindMemory(to: Int8.self)
return body(bytes.baseAddress!) // might crash if self.count == 0
}
}
}
+6
View File
@@ -36,6 +36,8 @@ extension Endpoint {
return "\(address):\(port)" return "\(address):\(port)"
case .ipv6(let address): case .ipv6(let address):
return "[\(address)]:\(port)" return "[\(address)]:\(port)"
@unknown default:
fatalError()
} }
} }
@@ -78,6 +80,8 @@ extension Endpoint {
return true return true
case .ipv6: case .ipv6:
return true return true
@unknown default:
fatalError()
} }
} }
@@ -89,6 +93,8 @@ extension Endpoint {
return nil return nil
case .ipv6: case .ipv6:
return nil return nil
@unknown default:
fatalError()
} }
} }
} }
@@ -22,6 +22,9 @@ extension NETunnelProviderProtocol {
if passwordReference == nil { if passwordReference == nil {
return nil return nil
} }
#if os(macOS)
providerConfiguration = ["UID": getuid()]
#endif
let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint } let endpoints = tunnelConfiguration.peers.compactMap { $0.endpoint }
if endpoints.count == 1 { if endpoints.count == 1 {
@@ -34,17 +37,14 @@ extension NETunnelProviderProtocol {
} }
func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? { func asTunnelConfiguration(called name: String? = nil) -> TunnelConfiguration? {
migrateConfigurationIfNeeded(called: name ?? "unknown") if let passwordReference = passwordReference,
//TODO: in the case where migrateConfigurationIfNeeded is called by the network extension, let config = Keychain.openReference(called: passwordReference) {
// before the app has started, and when there is, in fact, configuration that needs to be return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
// put into the keychain, this will generate one new keychain item every time it is started, }
// until finally the app is open. Would it be possible to call saveToPreferences here? Or is if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
// that generally not available to network extensions? In which case, what should our return try? TunnelConfiguration(fromWgQuickConfig: oldConfig, called: name)
// behavior be? }
return nil
guard let passwordReference = passwordReference else { return nil }
guard let config = Keychain.openReference(called: passwordReference) else { return nil }
return try? TunnelConfiguration(fromWgQuickConfig: config, called: name)
} }
func destroyConfigurationReference() { func destroyConfigurationReference() {
@@ -52,9 +52,9 @@ extension NETunnelProviderProtocol {
Keychain.deleteReference(called: ref) Keychain.deleteReference(called: ref)
} }
func verifyConfigurationReference() -> Data? { func verifyConfigurationReference() -> Bool {
guard let ref = passwordReference else { return nil } guard let ref = passwordReference else { return false }
return Keychain.verifyReference(called: ref) ? ref : nil return Keychain.verifyReference(called: ref)
} }
@discardableResult @discardableResult
@@ -63,10 +63,25 @@ extension NETunnelProviderProtocol {
* in the keychain. But it's still useful to keep the migration * in the keychain. But it's still useful to keep the migration
* around so that .mobileconfig files are easier. * around so that .mobileconfig files are easier.
*/ */
guard let oldConfig = providerConfiguration?["WgQuickConfig"] as? String else { return false } if let oldConfig = providerConfiguration?["WgQuickConfig"] as? String {
providerConfiguration = nil #if os(macOS)
guard passwordReference == nil else { return true } providerConfiguration = ["UID": getuid()]
passwordReference = Keychain.makeReference(containing: oldConfig, called: name) #elseif os(iOS)
return true providerConfiguration = nil
#else
#error("Unimplemented")
#endif
guard passwordReference == nil else { return true }
wg_log(.debug, message: "Migrating tunnel configuration '\(name)'")
passwordReference = Keychain.makeReference(containing: oldConfig, called: name)
return true
}
#if os(macOS)
if passwordReference != nil && providerConfiguration?["UID"] == nil && verifyConfigurationReference() {
providerConfiguration = ["UID": getuid()]
return true
}
#endif
return false
} }
} }
@@ -39,7 +39,7 @@ extension TunnelConfiguration {
var interfaceConfiguration: InterfaceConfiguration? var interfaceConfiguration: InterfaceConfiguration?
var peerConfigurations = [PeerConfiguration]() var peerConfigurations = [PeerConfiguration]()
let lines = wgQuickConfig.split(separator: "\n") let lines = wgQuickConfig.split { $0.isNewline }
var parserState = ParserState.notInASection var parserState = ParserState.notInASection
var attributes = [String: String]() var attributes = [String: String]()
@@ -52,15 +52,15 @@ extension TunnelConfiguration {
trimmedLine = String(line) trimmedLine = String(line)
} }
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces) trimmedLine = trimmedLine.trimmingCharacters(in: .whitespacesAndNewlines)
let lowercasedLine = trimmedLine.lowercased() let lowercasedLine = trimmedLine.lowercased()
if !trimmedLine.isEmpty { if !trimmedLine.isEmpty {
if let equalsIndex = trimmedLine.firstIndex(of: "=") { if let equalsIndex = trimmedLine.firstIndex(of: "=") {
// Line contains an attribute // Line contains an attribute
let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespaces) let keyWithCase = trimmedLine[..<equalsIndex].trimmingCharacters(in: .whitespacesAndNewlines)
let key = keyWithCase.lowercased() let key = keyWithCase.lowercased()
let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces) let value = trimmedLine[trimmedLine.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespacesAndNewlines)
let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"] let keysWithMultipleEntriesAllowed: Set<String> = ["address", "allowedips", "dns"]
if let presentValue = attributes[key] { if let presentValue = attributes[key] {
if keysWithMultipleEntriesAllowed.contains(key) { if keysWithMultipleEntriesAllowed.contains(key) {
@@ -182,7 +182,7 @@ extension TunnelConfiguration {
} }
if let addressesString = attributes["address"] { if let addressesString = attributes["address"] {
var addresses = [IPAddressRange]() var addresses = [IPAddressRange]()
for addressString in addressesString.splitToArray(trimmingCharacters: .whitespaces) { for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let address = IPAddressRange(from: addressString) else { guard let address = IPAddressRange(from: addressString) else {
throw ParseError.interfaceHasInvalidAddress(addressString) throw ParseError.interfaceHasInvalidAddress(addressString)
} }
@@ -192,7 +192,7 @@ extension TunnelConfiguration {
} }
if let dnsString = attributes["dns"] { if let dnsString = attributes["dns"] {
var dnsServers = [DNSServer]() var dnsServers = [DNSServer]()
for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespaces) { for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
guard let dnsServer = DNSServer(from: dnsServerString) else { guard let dnsServer = DNSServer(from: dnsServerString) else {
throw ParseError.interfaceHasInvalidDNS(dnsServerString) throw ParseError.interfaceHasInvalidDNS(dnsServerString)
} }
+236 -19
View File
@@ -41,12 +41,21 @@
6B62E45F220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */; }; 6B62E45F220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */; };
6B62E460220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */; }; 6B62E460220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */; };
6B653B86220DE2960050E69C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; }; 6B653B86220DE2960050E69C /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; };
6B6956362211DA80001B618A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B6956352211DA80001B618A /* main.m */; };
6B707D8421F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; }; 6B707D8421F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; };
6B707D8621F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; }; 6B707D8621F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */; };
6BD5C97B220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97B220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6BD5C97C220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97C220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6BD5C97D220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97D220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6BD5C97E220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; }; 6BD5C97E220D1AE200784E08 /* key.c in Sources */ = {isa = PBXBuildFile; fileRef = 6BD5C979220D1AE100784E08 /* key.c */; };
6F0F44C9222D55BB00B0FF04 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0F44C8222D55BB00B0FF04 /* TextCell.swift */; };
6F0F44CB222D55FD00B0FF04 /* EditableTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */; };
6F1075642258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1075632258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift */; };
6F19D30422402B8700A126F2 /* ConfirmationAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F19D30322402B8700A126F2 /* ConfirmationAlertPresenter.swift */; };
6F2449E8226587B90047B9E9 /* MacAppStoreUpdateDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2449E7226587B80047B9E9 /* MacAppStoreUpdateDetector.swift */; };
6F3E02E9228000F6001FE7E3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3E02E8228000F6001FE7E3 /* MainMenu.swift */; };
6F29A9432278518D00DC6A6B /* RecentTunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F29A9422278518D00DC6A6B /* RecentTunnelsTracker.swift */; };
6F29A94722787B1600DC6A6B /* QuickActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F29A94622787B1600DC6A6B /* QuickActionItem.swift */; };
6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */; }; 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */; };
6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */; }; 6F4DD16C21DA558F00690EAE /* NSTableView+Reuse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */; };
6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */; }; 6F4DD16E21DBEA0700690EAE /* ManageTunnelsRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */; };
@@ -54,15 +63,20 @@
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; }; 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5A2B4421AFDE020081EDD8 /* FileManager+Extension.swift */; };
6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; }; 6F5D0C1D218352EF000F85AD /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5D0C1C218352EF000F85AD /* PacketTunnelProvider.swift */; };
6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6F5D0C22218352EF000F85AD /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6F5EA59B223E58A8002B380A /* ButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5EA59A223E58A8002B380A /* ButtonRow.swift */; };
6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F613D9A21DE33B8004B217A /* KeyValueRow.swift */; }; 6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F613D9A21DE33B8004B217A /* KeyValueRow.swift */; };
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; }; 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1E821B932F700483816 /* WireGuardAppError.swift */; };
6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; }; 6F61F1EB21B937EF00483816 /* WireGuardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */; };
6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; }; 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; }; 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3E217F3413003482A3 /* DNSServer.swift */; };
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; }; 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */; };
6F6483E7229293300075BA15 /* LaunchedAtLoginDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6483E6229293300075BA15 /* LaunchedAtLoginDetector.swift */; };
6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; }; 6F6899A62180447E0012E523 /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; }; 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; }; 6F693A562179E556008551C1 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F693A552179E556008551C1 /* Endpoint.swift */; };
6F70E20E221058E1008BDFB4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6F70E20C221058DF008BDFB4 /* InfoPlist.strings */; };
6F70E20F221058E1008BDFB4 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6F70E20C221058DF008BDFB4 /* InfoPlist.strings */; };
6F70E23D22109E15008BDFB4 /* WireGuardLoginItemHelper.app in Embed Login Item Helper */ = {isa = PBXBuildFile; fileRef = 6F70E22922106A2D008BDFB4 /* WireGuardLoginItemHelper.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; }; 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774DF217181B1006A79B3 /* MainViewController.swift */; };
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; }; 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E0217181B1006A79B3 /* AppDelegate.swift */; };
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; }; 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */; };
@@ -73,13 +87,21 @@
6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; }; 6F7F7E5F21C7D74B00527607 /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; }; 6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */; };
6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */; }; 6F89E17C21F090CC00C97BB9 /* TunnelsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */; };
6F8F0D7122258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */; };
6F8F0D7222258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */; };
6F8F0D7422267AD2000E8335 /* ChevronCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7322267AD2000E8335 /* ChevronCell.swift */; };
6F8F0D7722267C57000E8335 /* SSIDOptionEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */; };
6F907C9C224663A2003CED21 /* LogViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F907C9B224663A2003CED21 /* LogViewHelper.swift */; };
6F907C9D224663A2003CED21 /* LogViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F907C9B224663A2003CED21 /* LogViewHelper.swift */; };
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; }; 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */; };
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; }; 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */; };
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; }; 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */; };
6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; }; 6F919EDB218C65C50023B400 /* wireguard_doc_logo_64x64.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */; };
6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; }; 6F919EDC218C65C50023B400 /* wireguard_doc_logo_320x320.png in Resources */ = {isa = PBXBuildFile; fileRef = 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */; };
6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B582721E8CD4300544D02 /* PopupRow.swift */; }; 6F9B8A8E223398610041B9C4 /* SSIDOptionDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B8A8D223398610041B9C4 /* SSIDOptionDetailTableViewController.swift */; };
6FADE96C2254B8C300B838A4 /* UnusableTunnelDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FADE96A2254A6C200B838A4 /* UnusableTunnelDetailViewController.swift */; };
6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; }; 6FB1017921C57DE600766195 /* MockTunnels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1017821C57DE600766195 /* MockTunnels.swift */; };
6FB17946222FD5960018AE71 /* OnDemandWiFiControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */; };
6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */; }; 6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */; };
6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FB1BD6121D2607E00A991BF /* Assets.xcassets */; }; 6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FB1BD6121D2607E00A991BF /* Assets.xcassets */; };
6FB1BD9921D4BFE700A991BF /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6FB1BD9121D4BFE600A991BF /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 6FB1BD9921D4BFE700A991BF /* WireGuardNetworkExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6FB1BD9121D4BFE600A991BF /* WireGuardNetworkExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -117,7 +139,7 @@
6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; }; 6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A52180447E0012E523 /* x25519.c */; };
6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; }; 6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6899A7218044FC0012E523 /* Curve25519.swift */; };
6FB1BDCC21D50F5300A991BF /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; }; 6FB1BDCC21D50F5300A991BF /* TunnelsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */; };
6FB1BDCD21D50F5300A991BF /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; }; 6FB1BDCD21D50F5300A991BF /* ActivateOnDemandOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandOption.swift */; };
6FB1BDCE21D50F5300A991BF /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; }; 6FB1BDCE21D50F5300A991BF /* TunnelStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4541A821C451D100994C13 /* TunnelStatus.swift */; };
6FB1BDD021D50F5300A991BF /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; }; 6FB1BDD021D50F5300A991BF /* TunnelErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */; };
6FB1BDD121D50F5300A991BF /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; }; 6FB1BDD121D50F5300A991BF /* ZipImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE254FA219C10800028284D /* ZipImporter.swift */; };
@@ -138,11 +160,14 @@
6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */; }; 6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */; };
6FBA104321D6BC250051C35F /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */; }; 6FBA104321D6BC250051C35F /* ErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */; };
6FBA104621D7EBFA0051C35F /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */; }; 6FBA104621D7EBFA0051C35F /* TunnelsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */; };
6FCD99AA21E0E14700BA4C82 /* NoTunnelsDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99A821E0E0C700BA4C82 /* NoTunnelsDetailViewController.swift */; }; 6FCD99AA21E0E14700BA4C82 /* ButtonedDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99A821E0E0C700BA4C82 /* ButtonedDetailViewController.swift */; };
6FCD99AF21E0EA1700BA4C82 /* ImportPanelPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */; }; 6FCD99AF21E0EA1700BA4C82 /* ImportPanelPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */; };
6FCD99B121E0EDA900BA4C82 /* TunnelEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */; }; 6FCD99B121E0EDA900BA4C82 /* TunnelEditViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */; };
6FDB3C3B21DCF47400A0C0BF /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */; }; 6FDB3C3B21DCF47400A0C0BF /* TunnelDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */; };
6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; }; 6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */; };
6FDB6D13224A15BF00EE4BC3 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */; };
6FDB6D15224CB2CE00EE4BC3 /* LogViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */; };
6FDB6D18224CC05A00EE4BC3 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */; };
6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; }; 6FDEF7E421846C1A00D8FBF6 /* libwg-go.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */; };
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; }; 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */; };
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; }; 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7F621863B6100D8FBF6 /* unzip.c */; };
@@ -170,7 +195,7 @@
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; }; 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; }; 6FFA5D96219446380001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */; };
6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; }; 6FFA5DA021958ECC0001E2F7 /* ErrorNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */; };
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */; }; 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandOption.swift */; };
6FFACD2021E4D8D500E9A2A5 /* ParseError+WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFACD1E21E4D89600E9A2A5 /* ParseError+WireGuardAppError.swift */; }; 6FFACD2021E4D8D500E9A2A5 /* ParseError+WireGuardAppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FFACD1E21E4D89600E9A2A5 /* ParseError+WireGuardAppError.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -182,6 +207,13 @@
remoteGlobalIDString = 6F5D0C19218352EF000F85AD; remoteGlobalIDString = 6F5D0C19218352EF000F85AD;
remoteInfo = WireGuardNetworkExtension; remoteInfo = WireGuardNetworkExtension;
}; };
6F70E23A22109DD3008BDFB4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 6FF4AC0C211EC46F002C96EB /* Project object */;
proxyType = 1;
remoteGlobalIDString = 6F70E22822106A2D008BDFB4;
remoteInfo = WireGuardmacOSLoginItemHelper;
};
6FB1BD9721D4BFE700A991BF /* PBXContainerItemProxy */ = { 6FB1BD9721D4BFE700A991BF /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 6FF4AC0C211EC46F002C96EB /* Project object */; containerPortal = 6FF4AC0C211EC46F002C96EB /* Project object */;
@@ -217,6 +249,17 @@
name = "Embed App Extensions"; name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
6F70E23C22109DE5008BDFB4 /* Embed Login Item Helper */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = Contents/Library/LoginItems;
dstSubfolderSpec = 1;
files = (
6F70E23D22109E15008BDFB4 /* WireGuardLoginItemHelper.app in Embed Login Item Helper */,
);
name = "Embed Login Item Helper";
runOnlyForDeploymentPostprocessing = 0;
};
6FB1BD9D21D4BFE700A991BF /* Embed App Extensions */ = { 6FB1BD9D21D4BFE700A991BF /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -252,9 +295,18 @@
6B586C52220CBA6D00427C51 /* Data+KeyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+KeyEncoding.swift"; sourceTree = "<group>"; }; 6B586C52220CBA6D00427C51 /* Data+KeyEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+KeyEncoding.swift"; sourceTree = "<group>"; };
6B5C5E26220A48D30024272E /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; }; 6B5C5E26220A48D30024272E /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConfirmation.swift; sourceTree = "<group>"; }; 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConfirmation.swift; sourceTree = "<group>"; };
6B6956352211DA80001B618A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+UapiConfig.swift"; sourceTree = "<group>"; }; 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelConfiguration+UapiConfig.swift"; sourceTree = "<group>"; };
6BD5C979220D1AE100784E08 /* key.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key.c; sourceTree = "<group>"; }; 6BD5C979220D1AE100784E08 /* key.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key.c; sourceTree = "<group>"; };
6BD5C97A220D1AE200784E08 /* key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = key.h; sourceTree = "<group>"; }; 6BD5C97A220D1AE200784E08 /* key.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = key.h; sourceTree = "<group>"; };
6F0F44C8222D55BB00B0FF04 /* TextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = "<group>"; };
6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextCell.swift; sourceTree = "<group>"; };
6F1075632258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteTunnelsConfirmationAlert.swift; sourceTree = "<group>"; };
6F19D30322402B8700A126F2 /* ConfirmationAlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationAlertPresenter.swift; sourceTree = "<group>"; };
6F2449E7226587B80047B9E9 /* MacAppStoreUpdateDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAppStoreUpdateDetector.swift; sourceTree = "<group>"; };
6F3E02E8228000F6001FE7E3 /* MainMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
6F29A9422278518D00DC6A6B /* RecentTunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentTunnelsTracker.swift; sourceTree = "<group>"; };
6F29A94622787B1600DC6A6B /* QuickActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuickActionItem.swift; sourceTree = "<group>"; };
6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = "<group>"; }; 6F4DD16721DA552B00690EAE /* NSTableView+Reuse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Reuse.swift"; sourceTree = "<group>"; };
6F4DD16A21DA558800690EAE /* TunnelListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListRow.swift; sourceTree = "<group>"; }; 6F4DD16A21DA558800690EAE /* TunnelListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelListRow.swift; sourceTree = "<group>"; };
6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = "<group>"; }; 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageTunnelsRootViewController.swift; sourceTree = "<group>"; };
@@ -266,17 +318,23 @@
6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension_iOS.entitlements; sourceTree = "<group>"; }; 6F5D0C1F218352EF000F85AD /* WireGuardNetworkExtension_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuardNetworkExtension_iOS.entitlements; sourceTree = "<group>"; };
6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; }; 6F5D0C3421839E37000F85AD /* WireGuardNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuardNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; }; 6F5D0C472183C6A3000F85AD /* PacketTunnelSettingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelSettingsGenerator.swift; sourceTree = "<group>"; };
6F5EA59A223E58A8002B380A /* ButtonRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonRow.swift; sourceTree = "<group>"; };
6F613D9A21DE33B8004B217A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = "<group>"; }; 6F613D9A21DE33B8004B217A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = "<group>"; };
6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; }; 6F61F1E821B932F700483816 /* WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardAppError.swift; sourceTree = "<group>"; };
6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; }; 6F61F1EA21B937EF00483816 /* WireGuardResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardResult.swift; sourceTree = "<group>"; };
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; }; 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewModel.swift; sourceTree = "<group>"; };
6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; }; 6F628C3E217F3413003482A3 /* DNSServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSServer.swift; sourceTree = "<group>"; };
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; }; 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
6F6483E6229293300075BA15 /* LaunchedAtLoginDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchedAtLoginDetector.swift; sourceTree = "<group>"; };
6F689999218043390012E523 /* WireGuard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuard-Bridging-Header.h"; sourceTree = "<group>"; }; 6F689999218043390012E523 /* WireGuard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WireGuard-Bridging-Header.h"; sourceTree = "<group>"; };
6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; }; 6F6899A42180447E0012E523 /* x25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = x25519.h; sourceTree = "<group>"; };
6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; }; 6F6899A52180447E0012E523 /* x25519.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = x25519.c; sourceTree = "<group>"; };
6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; }; 6F6899A7218044FC0012E523 /* Curve25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Curve25519.swift; sourceTree = "<group>"; };
6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; }; 6F693A552179E556008551C1 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = "<group>"; };
6F70E20D221058DF008BDFB4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = WireGuard/Base.lproj/InfoPlist.strings; sourceTree = "<group>"; };
6F70E22922106A2D008BDFB4 /* WireGuardLoginItemHelper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuardLoginItemHelper.app; sourceTree = BUILT_PRODUCTS_DIR; };
6F70E23222106A31008BDFB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6F70E23922109BEF008BDFB4 /* LoginItemHelper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LoginItemHelper.entitlements; sourceTree = "<group>"; };
6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; }; 6F7774DF217181B1006A79B3 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 6F7774E0217181B1006A79B3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; }; 6F7774E321718281006A79B3 /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
@@ -287,13 +345,19 @@
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; }; 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelErrors.swift; sourceTree = "<group>"; };
6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; }; 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemController.swift; sourceTree = "<group>"; };
6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = "<group>"; }; 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsTracker.swift; sourceTree = "<group>"; };
6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandViewModel.swift; sourceTree = "<group>"; };
6F8F0D7322267AD2000E8335 /* ChevronCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronCell.swift; sourceTree = "<group>"; };
6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSIDOptionEditTableViewController.swift; sourceTree = "<group>"; };
6F907C9B224663A2003CED21 /* LogViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewHelper.swift; sourceTree = "<group>"; };
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; }; 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; }; 6F919ED5218C65C50023B400 /* wireguard_doc_logo_22x29.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_22x29.png; sourceTree = "<group>"; };
6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; }; 6F919ED6218C65C50023B400 /* wireguard_doc_logo_44x58.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_44x58.png; sourceTree = "<group>"; };
6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = "<group>"; }; 6F919ED7218C65C50023B400 /* wireguard_doc_logo_64x64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_64x64.png; sourceTree = "<group>"; };
6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; }; 6F919ED8218C65C50023B400 /* wireguard_doc_logo_320x320.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wireguard_doc_logo_320x320.png; sourceTree = "<group>"; };
6F9B582721E8CD4300544D02 /* PopupRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupRow.swift; sourceTree = "<group>"; }; 6F9B8A8D223398610041B9C4 /* SSIDOptionDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSIDOptionDetailTableViewController.swift; sourceTree = "<group>"; };
6FADE96A2254A6C200B838A4 /* UnusableTunnelDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnusableTunnelDetailViewController.swift; sourceTree = "<group>"; };
6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; }; 6FB1017821C57DE600766195 /* MockTunnels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTunnels.swift; sourceTree = "<group>"; };
6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandWiFiControls.swift; sourceTree = "<group>"; };
6FB1BD5D21D2607A00A991BF /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6FB1BD5D21D2607A00A991BF /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; };
6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
6FB1BD6121D2607E00A991BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 6FB1BD6121D2607E00A991BF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -309,10 +373,13 @@
6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelImporter.swift; sourceTree = "<group>"; }; 6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelImporter.swift; sourceTree = "<group>"; };
6FBA104121D6BC210051C35F /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; }; 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPresenter.swift; sourceTree = "<group>"; };
6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; }; 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelsListTableViewController.swift; sourceTree = "<group>"; };
6FCD99A821E0E0C700BA4C82 /* NoTunnelsDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoTunnelsDetailViewController.swift; sourceTree = "<group>"; }; 6FCD99A821E0E0C700BA4C82 /* ButtonedDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonedDetailViewController.swift; sourceTree = "<group>"; };
6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPanelPresenter.swift; sourceTree = "<group>"; }; 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportPanelPresenter.swift; sourceTree = "<group>"; };
6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditViewController.swift; sourceTree = "<group>"; }; 6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelEditViewController.swift; sourceTree = "<group>"; };
6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; }; 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelDetailTableViewController.swift; sourceTree = "<group>"; };
6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewCell.swift; sourceTree = "<group>"; };
6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 6FDEF7E321846C1A00D8FBF6 /* libwg-go.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; };
6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; }; 6FDEF7E52185EFAF00D8FBF6 /* QRScanViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; };
6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; }; 6FDEF7F621863B6100D8FBF6 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unzip.c; sourceTree = "<group>"; };
@@ -340,7 +407,7 @@
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; }; 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WireGuard.entitlements; sourceTree = "<group>"; };
6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; }; 6FFA5D942194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NETunnelProviderProtocol+Extension.swift"; sourceTree = "<group>"; };
6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; }; 6FFA5D9F21958ECC0001E2F7 /* ErrorNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorNotifier.swift; sourceTree = "<group>"; };
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandSetting.swift; sourceTree = "<group>"; }; 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateOnDemandOption.swift; sourceTree = "<group>"; };
6FFACD1E21E4D89600E9A2A5 /* ParseError+WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseError+WireGuardAppError.swift"; sourceTree = "<group>"; }; 6FFACD1E21E4D89600E9A2A5 /* ParseError+WireGuardAppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseError+WireGuardAppError.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -392,6 +459,9 @@
5F4541A521C4449E00994C13 /* ButtonCell.swift */, 5F4541A521C4449E00994C13 /* ButtonCell.swift */,
5F45419121C2D55800994C13 /* CheckmarkCell.swift */, 5F45419121C2D55800994C13 /* CheckmarkCell.swift */,
5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */, 5F4541A121C2D6DF00994C13 /* BorderedTextButton.swift */,
6F8F0D7322267AD2000E8335 /* ChevronCell.swift */,
6F0F44C8222D55BB00B0FF04 /* TextCell.swift */,
6F0F44CA222D55FD00B0FF04 /* EditableTextCell.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -405,6 +475,9 @@
6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */, 6F628C40217F47DB003482A3 /* TunnelDetailTableViewController.swift */,
6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */, 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */,
6F7774DF217181B1006A79B3 /* MainViewController.swift */, 6F7774DF217181B1006A79B3 /* MainViewController.swift */,
6F8F0D7622267C57000E8335 /* SSIDOptionEditTableViewController.swift */,
6F9B8A8D223398610041B9C4 /* SSIDOptionDetailTableViewController.swift */,
6FDB6D16224CC04E00EE4BC3 /* LogViewController.swift */,
); );
path = ViewController; path = ViewController;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -418,8 +491,11 @@
6F613D9A21DE33B8004B217A /* KeyValueRow.swift */, 6F613D9A21DE33B8004B217A /* KeyValueRow.swift */,
5F52D0BA21E3781B00283CEA /* ConfTextView.swift */, 5F52D0BA21E3781B00283CEA /* ConfTextView.swift */,
5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */, 5F52D0BC21E3785C00283CEA /* ConfTextStorage.swift */,
6F9B582721E8CD4300544D02 /* PopupRow.swift */,
6FE3661C21F64F6B00F78C7D /* ConfTextColorTheme.swift */, 6FE3661C21F64F6B00F78C7D /* ConfTextColorTheme.swift */,
6F5EA59A223E58A8002B380A /* ButtonRow.swift */,
6FB17945222FD5960018AE71 /* OnDemandWiFiControls.swift */,
6FDB6D14224CB2CE00EE4BC3 /* LogViewCell.swift */,
6F1075632258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift */,
); );
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -460,15 +536,27 @@
path = Crypto; path = Crypto;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6F70E22A22106A2D008BDFB4 /* LoginItemHelper */ = {
isa = PBXGroup;
children = (
6F70E23922109BEF008BDFB4 /* LoginItemHelper.entitlements */,
6F70E23222106A31008BDFB4 /* Info.plist */,
6B6956352211DA80001B618A /* main.m */,
);
path = LoginItemHelper;
sourceTree = "<group>";
};
6F7774DD217181B1006A79B3 /* UI */ = { 6F7774DD217181B1006A79B3 /* UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6FB1BD5E21D2607A00A991BF /* macOS */, 6FB1BD5E21D2607A00A991BF /* macOS */,
6F7774DE217181B1006A79B3 /* iOS */, 6F7774DE217181B1006A79B3 /* iOS */,
6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */, 6F628C3C217F09E9003482A3 /* TunnelViewModel.swift */,
6F8F0D7022258153000E8335 /* ActivateOnDemandViewModel.swift */,
6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */, 6FBA103D21D6B6D70051C35F /* TunnelImporter.swift */,
6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */, 6FBA103A21D6B4280051C35F /* ErrorPresenterProtocol.swift */,
6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */, 6B62E45E220A6FA900EF34A6 /* PrivateDataConfirmation.swift */,
6F907C9B224663A2003CED21 /* LogViewHelper.swift */,
); );
path = UI; path = UI;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -478,11 +566,14 @@
children = ( children = (
6FF4AC1E211EC472002C96EB /* Assets.xcassets */, 6FF4AC1E211EC472002C96EB /* Assets.xcassets */,
6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */, 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */,
5F4541AC21C4720B00994C13 /* ViewController */,
5F4541A721C44F5B00994C13 /* View */, 5F4541A721C44F5B00994C13 /* View */,
5F4541AC21C4720B00994C13 /* ViewController */,
6F7774E0217181B1006A79B3 /* AppDelegate.swift */, 6F7774E0217181B1006A79B3 /* AppDelegate.swift */,
6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */, 6F919EC2218A2AE90023B400 /* ErrorPresenter.swift */,
6F19D30322402B8700A126F2 /* ConfirmationAlertPresenter.swift */,
5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */, 5F45417C21C1B23600994C13 /* UITableViewCell+Reuse.swift */,
6F29A9422278518D00DC6A6B /* RecentTunnelsTracker.swift */,
6F29A94622787B1600DC6A6B /* QuickActionItem.swift */,
6FF4AC23211EC472002C96EB /* Info.plist */, 6FF4AC23211EC472002C96EB /* Info.plist */,
6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */, 6FF4AC482120B9E0002C96EB /* WireGuard.entitlements */,
); );
@@ -513,7 +604,7 @@
children = ( children = (
6F7774EE21722D97006A79B3 /* TunnelsManager.swift */, 6F7774EE21722D97006A79B3 /* TunnelsManager.swift */,
6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */, 6B707D8321F918D4000A8F73 /* TunnelConfiguration+UapiConfig.swift */,
6FFA5DA32197085D0001E2F7 /* ActivateOnDemandSetting.swift */, 6FFA5DA32197085D0001E2F7 /* ActivateOnDemandOption.swift */,
5F4541A821C451D100994C13 /* TunnelStatus.swift */, 5F4541A821C451D100994C13 /* TunnelStatus.swift */,
6FB1017821C57DE600766195 /* MockTunnels.swift */, 6FB1017821C57DE600766195 /* MockTunnels.swift */,
6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */, 6F7F7E5E21C7D74B00527607 /* TunnelErrors.swift */,
@@ -543,15 +634,19 @@
6FB1BD5E21D2607A00A991BF /* macOS */ = { 6FB1BD5E21D2607A00A991BF /* macOS */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F70E22A22106A2D008BDFB4 /* LoginItemHelper */,
6F4DD16921DA556600690EAE /* View */, 6F4DD16921DA556600690EAE /* View */,
6FBA104421D7EA750051C35F /* ViewController */, 6FBA104421D7EA750051C35F /* ViewController */,
6FBA101321D613F30051C35F /* Application.swift */, 6FBA101321D613F30051C35F /* Application.swift */,
6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */, 6FB1BD5F21D2607A00A991BF /* AppDelegate.swift */,
6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */, 6F89E17921EDEB0E00C97BB9 /* StatusItemController.swift */,
6FBA101621D655340051C35F /* StatusMenu.swift */, 6FBA101621D655340051C35F /* StatusMenu.swift */,
6F3E02E8228000F6001FE7E3 /* MainMenu.swift */,
6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */, 6F89E17B21F090CC00C97BB9 /* TunnelsTracker.swift */,
6FBA104121D6BC210051C35F /* ErrorPresenter.swift */, 6FBA104121D6BC210051C35F /* ErrorPresenter.swift */,
6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */, 6FCD99AE21E0EA1700BA4C82 /* ImportPanelPresenter.swift */,
6F2449E7226587B80047B9E9 /* MacAppStoreUpdateDetector.swift */,
6F6483E6229293300075BA15 /* LaunchedAtLoginDetector.swift */,
6FB1BD6121D2607E00A991BF /* Assets.xcassets */, 6FB1BD6121D2607E00A991BF /* Assets.xcassets */,
6FB1BD6621D2607E00A991BF /* Info.plist */, 6FB1BD6621D2607E00A991BF /* Info.plist */,
6FB1BD6721D2607E00A991BF /* WireGuard.entitlements */, 6FB1BD6721D2607E00A991BF /* WireGuard.entitlements */,
@@ -568,8 +663,10 @@
6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */, 6FBA104521D7EBFA0051C35F /* TunnelsListTableViewController.swift */,
6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */, 6F4DD16D21DBEA0700690EAE /* ManageTunnelsRootViewController.swift */,
6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */, 6FDB3C3A21DCF47400A0C0BF /* TunnelDetailTableViewController.swift */,
6FCD99A821E0E0C700BA4C82 /* NoTunnelsDetailViewController.swift */, 6FCD99A821E0E0C700BA4C82 /* ButtonedDetailViewController.swift */,
6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */, 6FCD99B021E0EDA900BA4C82 /* TunnelEditViewController.swift */,
6FDB6D12224A15BE00EE4BC3 /* LogViewController.swift */,
6FADE96A2254A6C200B838A4 /* UnusableTunnelDetailViewController.swift */,
); );
path = ViewController; path = ViewController;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -619,6 +716,7 @@
6FF4AC0B211EC46F002C96EB = { 6FF4AC0B211EC46F002C96EB = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
6F70E20C221058DF008BDFB4 /* InfoPlist.strings */,
6FE1765421C90BBE002690EA /* Localizable.strings */, 6FE1765421C90BBE002690EA /* Localizable.strings */,
6F5D0C432183B4A4000F85AD /* Shared */, 6F5D0C432183B4A4000F85AD /* Shared */,
6FF4AC16211EC46F002C96EB /* WireGuard */, 6FF4AC16211EC46F002C96EB /* WireGuard */,
@@ -635,6 +733,7 @@
6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */, 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */,
6FB1BD5D21D2607A00A991BF /* WireGuard.app */, 6FB1BD5D21D2607A00A991BF /* WireGuard.app */,
6FB1BD9121D4BFE600A991BF /* WireGuardNetworkExtension.appex */, 6FB1BD9121D4BFE600A991BF /* WireGuardNetworkExtension.appex */,
6F70E22922106A2D008BDFB4 /* WireGuardLoginItemHelper.app */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -721,6 +820,21 @@
productReference = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */; productReference = 6F5D0C1A218352EF000F85AD /* WireGuardNetworkExtension.appex */;
productType = "com.apple.product-type.app-extension"; productType = "com.apple.product-type.app-extension";
}; };
6F70E22822106A2D008BDFB4 /* WireGuardmacOSLoginItemHelper */ = {
isa = PBXNativeTarget;
buildConfigurationList = 6F70E23622106A31008BDFB4 /* Build configuration list for PBXNativeTarget "WireGuardmacOSLoginItemHelper" */;
buildPhases = (
6F70E22522106A2D008BDFB4 /* Sources */,
);
buildRules = (
);
dependencies = (
);
name = WireGuardmacOSLoginItemHelper;
productName = WireGuardmacOSLoginItemHelper;
productReference = 6F70E22922106A2D008BDFB4 /* WireGuardLoginItemHelper.app */;
productType = "com.apple.product-type.application";
};
6FB1BD5C21D2607A00A991BF /* WireGuardmacOS */ = { 6FB1BD5C21D2607A00A991BF /* WireGuardmacOS */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 6FB1BD6A21D2607E00A991BF /* Build configuration list for PBXNativeTarget "WireGuardmacOS" */; buildConfigurationList = 6FB1BD6A21D2607E00A991BF /* Build configuration list for PBXNativeTarget "WireGuardmacOS" */;
@@ -732,10 +846,12 @@
6FB1BD5A21D2607A00A991BF /* Frameworks */, 6FB1BD5A21D2607A00A991BF /* Frameworks */,
6FB1BD5B21D2607A00A991BF /* Resources */, 6FB1BD5B21D2607A00A991BF /* Resources */,
6FB1BD9D21D4BFE700A991BF /* Embed App Extensions */, 6FB1BD9D21D4BFE700A991BF /* Embed App Extensions */,
6F70E23C22109DE5008BDFB4 /* Embed Login Item Helper */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
6F70E23B22109DD3008BDFB4 /* PBXTargetDependency */,
6FB1BD9821D4BFE700A991BF /* PBXTargetDependency */, 6FB1BD9821D4BFE700A991BF /* PBXTargetDependency */,
); );
name = WireGuardmacOS; name = WireGuardmacOS;
@@ -797,10 +913,19 @@
TargetAttributes = { TargetAttributes = {
6F5D0C19218352EF000F85AD = { 6F5D0C19218352EF000F85AD = {
CreatedOnToolsVersion = 10.0; CreatedOnToolsVersion = 10.0;
LastSwiftMigration = 1000; LastSwiftMigration = 1020;
};
6F70E22822106A2D008BDFB4 = {
CreatedOnToolsVersion = 10.1;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
}; };
6FB1BD5C21D2607A00A991BF = { 6FB1BD5C21D2607A00A991BF = {
CreatedOnToolsVersion = 10.1; CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1020;
SystemCapabilities = { SystemCapabilities = {
com.apple.ApplicationGroups.Mac = { com.apple.ApplicationGroups.Mac = {
enabled = 1; enabled = 1;
@@ -815,6 +940,7 @@
}; };
6FB1BD9021D4BFE600A991BF = { 6FB1BD9021D4BFE600A991BF = {
CreatedOnToolsVersion = 10.1; CreatedOnToolsVersion = 10.1;
LastSwiftMigration = 1020;
SystemCapabilities = { SystemCapabilities = {
com.apple.ApplicationGroups.Mac = { com.apple.ApplicationGroups.Mac = {
enabled = 1; enabled = 1;
@@ -826,8 +952,11 @@
}; };
6FF4AC13211EC46F002C96EB = { 6FF4AC13211EC46F002C96EB = {
CreatedOnToolsVersion = 9.4.1; CreatedOnToolsVersion = 9.4.1;
LastSwiftMigration = 1000; LastSwiftMigration = 1020;
SystemCapabilities = { SystemCapabilities = {
com.apple.AccessWiFi = {
enabled = 1;
};
com.apple.ApplicationGroups.iOS = { com.apple.ApplicationGroups.iOS = {
enabled = 1; enabled = 1;
}; };
@@ -857,6 +986,7 @@
6FB1BD5C21D2607A00A991BF /* WireGuardmacOS */, 6FB1BD5C21D2607A00A991BF /* WireGuardmacOS */,
6FB1BD9021D4BFE600A991BF /* WireGuardNetworkExtensionmacOS */, 6FB1BD9021D4BFE600A991BF /* WireGuardNetworkExtensionmacOS */,
6FDAA03421CE69D000FA6925 /* WireGuardGoBridgemacOS */, 6FDAA03421CE69D000FA6925 /* WireGuardGoBridgemacOS */,
6F70E22822106A2D008BDFB4 /* WireGuardmacOSLoginItemHelper */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -874,6 +1004,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */, 6FB1BD6221D2607E00A991BF /* Assets.xcassets in Resources */,
6F70E20F221058E1008BDFB4 /* InfoPlist.strings in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -889,6 +1020,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */, 6F919ED9218C65C50023B400 /* wireguard_doc_logo_22x29.png in Resources */,
6F70E20E221058E1008BDFB4 /* InfoPlist.strings in Resources */,
6FE1765621C90BBE002690EA /* Localizable.strings in Resources */, 6FE1765621C90BBE002690EA /* Localizable.strings in Resources */,
6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */, 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */,
6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */, 6F919EDA218C65C50023B400 /* wireguard_doc_logo_44x58.png in Resources */,
@@ -1114,13 +1246,22 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
6F70E22522106A2D008BDFB4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6B6956362211DA80001B618A /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6FB1BD5921D2607A00A991BF /* Sources */ = { 6FB1BD5921D2607A00A991BF /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FBA101521D613F90051C35F /* Application.swift in Sources */, 6FBA101521D613F90051C35F /* Application.swift in Sources */,
6FB1BDCC21D50F5300A991BF /* TunnelsManager.swift in Sources */, 6FB1BDCC21D50F5300A991BF /* TunnelsManager.swift in Sources */,
6FB1BDCD21D50F5300A991BF /* ActivateOnDemandSetting.swift in Sources */, 6F8F0D7222258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */,
6FB1BDCD21D50F5300A991BF /* ActivateOnDemandOption.swift in Sources */,
6FB1BDCE21D50F5300A991BF /* TunnelStatus.swift in Sources */, 6FB1BDCE21D50F5300A991BF /* TunnelStatus.swift in Sources */,
6FB1BDD021D50F5300A991BF /* TunnelErrors.swift in Sources */, 6FB1BDD021D50F5300A991BF /* TunnelErrors.swift in Sources */,
6FB1BDD121D50F5300A991BF /* ZipImporter.swift in Sources */, 6FB1BDD121D50F5300A991BF /* ZipImporter.swift in Sources */,
@@ -1129,6 +1270,7 @@
6FB1BDD321D50F5300A991BF /* ZipArchive.swift in Sources */, 6FB1BDD321D50F5300A991BF /* ZipArchive.swift in Sources */,
6FB1BDD421D50F5300A991BF /* ioapi.c in Sources */, 6FB1BDD421D50F5300A991BF /* ioapi.c in Sources */,
6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */, 6FDB3C3C21DCF6BB00A0C0BF /* TunnelViewModel.swift in Sources */,
6FDB6D13224A15BF00EE4BC3 /* LogViewController.swift in Sources */,
6B5C5E29220A48D30024272E /* Keychain.swift in Sources */, 6B5C5E29220A48D30024272E /* Keychain.swift in Sources */,
6FCD99AF21E0EA1700BA4C82 /* ImportPanelPresenter.swift in Sources */, 6FCD99AF21E0EA1700BA4C82 /* ImportPanelPresenter.swift in Sources */,
6FB1BDD521D50F5300A991BF /* unzip.c in Sources */, 6FB1BDD521D50F5300A991BF /* unzip.c in Sources */,
@@ -1149,31 +1291,39 @@
6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */, 6FB1BDCA21D50F1700A991BF /* x25519.c in Sources */,
6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */, 6FB1BDCB21D50F1700A991BF /* Curve25519.swift in Sources */,
6B586C55220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */, 6B586C55220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */,
6F9B582921E8D6D100544D02 /* PopupRow.swift in Sources */, 6FB17946222FD5960018AE71 /* OnDemandWiFiControls.swift in Sources */,
6FB1BDBB21D50F0200A991BF /* Localizable.strings in Sources */, 6FB1BDBB21D50F0200A991BF /* Localizable.strings in Sources */,
6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */, 6FB1BDBC21D50F0200A991BF /* ringlogger.c in Sources */,
6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */, 6FB1BDBD21D50F0200A991BF /* ringlogger.h in Sources */,
6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */, 6FBA103F21D6B6FF0051C35F /* TunnelImporter.swift in Sources */,
6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */, 6F89E17A21EDEB0E00C97BB9 /* StatusItemController.swift in Sources */,
6F5EA59B223E58A8002B380A /* ButtonRow.swift in Sources */,
6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */, 6F4DD16B21DA558800690EAE /* TunnelListRow.swift in Sources */,
6FDB6D15224CB2CE00EE4BC3 /* LogViewCell.swift in Sources */,
6FE3661D21F64F6B00F78C7D /* ConfTextColorTheme.swift in Sources */, 6FE3661D21F64F6B00F78C7D /* ConfTextColorTheme.swift in Sources */,
6F3E02E9228000F6001FE7E3 /* MainMenu.swift in Sources */,
5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */, 5F52D0BF21E3788900283CEA /* NSColor+Hex.swift in Sources */,
6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */, 6FB1BDBE21D50F0200A991BF /* Logger.swift in Sources */,
6F6483E7229293300075BA15 /* LaunchedAtLoginDetector.swift in Sources */,
6FB1BDBF21D50F0200A991BF /* TunnelConfiguration+WgQuickConfig.swift in Sources */, 6FB1BDBF21D50F0200A991BF /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
6FADE96C2254B8C300B838A4 /* UnusableTunnelDetailViewController.swift in Sources */,
6FFACD2021E4D8D500E9A2A5 /* ParseError+WireGuardAppError.swift in Sources */, 6FFACD2021E4D8D500E9A2A5 /* ParseError+WireGuardAppError.swift in Sources */,
6FB1BDC021D50F0200A991BF /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FB1BDC021D50F0200A991BF /* NETunnelProviderProtocol+Extension.swift in Sources */,
6F1075642258AE9800D78929 /* DeleteTunnelsConfirmationAlert.swift in Sources */,
6FBA101821D656000051C35F /* StatusMenu.swift in Sources */, 6FBA101821D656000051C35F /* StatusMenu.swift in Sources */,
6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */, 6F613D9B21DE33B8004B217A /* KeyValueRow.swift in Sources */,
6FB1BDC121D50F0200A991BF /* String+ArrayConversion.swift in Sources */, 6FB1BDC121D50F0200A991BF /* String+ArrayConversion.swift in Sources */,
5F52D0BB21E3781B00283CEA /* ConfTextView.swift in Sources */, 5F52D0BB21E3781B00283CEA /* ConfTextView.swift in Sources */,
6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */, 6FBA104021D6B7040051C35F /* ErrorPresenterProtocol.swift in Sources */,
6FCD99AA21E0E14700BA4C82 /* NoTunnelsDetailViewController.swift in Sources */, 6FCD99AA21E0E14700BA4C82 /* ButtonedDetailViewController.swift in Sources */,
6FB1BDC321D50F0300A991BF /* TunnelConfiguration.swift in Sources */, 6FB1BDC321D50F0300A991BF /* TunnelConfiguration.swift in Sources */,
6FB1BDC421D50F0300A991BF /* IPAddressRange.swift in Sources */, 6FB1BDC421D50F0300A991BF /* IPAddressRange.swift in Sources */,
6FBA104321D6BC250051C35F /* ErrorPresenter.swift in Sources */, 6FBA104321D6BC250051C35F /* ErrorPresenter.swift in Sources */,
6FB1BDC521D50F0300A991BF /* Endpoint.swift in Sources */, 6FB1BDC521D50F0300A991BF /* Endpoint.swift in Sources */,
6FB1BDC621D50F0300A991BF /* DNSServer.swift in Sources */, 6FB1BDC621D50F0300A991BF /* DNSServer.swift in Sources */,
6F2449E8226587B90047B9E9 /* MacAppStoreUpdateDetector.swift in Sources */,
6FB1BDC721D50F0300A991BF /* InterfaceConfiguration.swift in Sources */, 6FB1BDC721D50F0300A991BF /* InterfaceConfiguration.swift in Sources */,
6F907C9D224663A2003CED21 /* LogViewHelper.swift in Sources */,
6FB1BDC821D50F0300A991BF /* PeerConfiguration.swift in Sources */, 6FB1BDC821D50F0300A991BF /* PeerConfiguration.swift in Sources */,
6FB1BDC921D50F0300A991BF /* FileManager+Extension.swift in Sources */, 6FB1BDC921D50F0300A991BF /* FileManager+Extension.swift in Sources */,
6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */, 6FB1BD6021D2607A00A991BF /* AppDelegate.swift in Sources */,
@@ -1211,19 +1361,25 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */, 6FE1765A21C90E87002690EA /* LocalizationHelper.swift in Sources */,
6F29A94722787B1600DC6A6B /* QuickActionItem.swift in Sources */,
6FF3527221C2616C0008484E /* ringlogger.c in Sources */, 6FF3527221C2616C0008484E /* ringlogger.c in Sources */,
6F0F44CB222D55FD00B0FF04 /* EditableTextCell.swift in Sources */,
6FF3527321C2616C0008484E /* Logger.swift in Sources */, 6FF3527321C2616C0008484E /* Logger.swift in Sources */,
6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */, 6F7774E421718281006A79B3 /* TunnelsListTableViewController.swift in Sources */,
6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */, 6F7774EF21722D97006A79B3 /* TunnelsManager.swift in Sources */,
5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */, 5F45417D21C1B23600994C13 /* UITableViewCell+Reuse.swift in Sources */,
5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */, 5F45419221C2D55800994C13 /* CheckmarkCell.swift in Sources */,
6FE254FF219C60290028284D /* ZipExporter.swift in Sources */, 6FE254FF219C60290028284D /* ZipExporter.swift in Sources */,
6F8F0D7122258153000E8335 /* ActivateOnDemandViewModel.swift in Sources */,
6F8F0D7722267C57000E8335 /* SSIDOptionEditTableViewController.swift in Sources */,
6B586C53220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */, 6B586C53220CBA6D00427C51 /* Data+KeyEncoding.swift in Sources */,
6F693A562179E556008551C1 /* Endpoint.swift in Sources */, 6F693A562179E556008551C1 /* Endpoint.swift in Sources */,
6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */, 6FDEF7E62185EFB200D8FBF6 /* QRScanViewController.swift in Sources */,
6FDB6D18224CC05A00EE4BC3 /* LogViewController.swift in Sources */,
6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */, 6FFA5D952194454A0001E2F7 /* NETunnelProviderProtocol+Extension.swift in Sources */,
5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */, 5FF7B96221CC95DE00A7DD74 /* InterfaceConfiguration.swift in Sources */,
5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */, 5F4541A921C451D100994C13 /* TunnelStatus.swift in Sources */,
6F8F0D7422267AD2000E8335 /* ChevronCell.swift in Sources */,
6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */, 6F61F1E921B932F700483816 /* WireGuardAppError.swift in Sources */,
6F6899A62180447E0012E523 /* x25519.c in Sources */, 6F6899A62180447E0012E523 /* x25519.c in Sources */,
6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */, 6F7774E2217181B1006A79B3 /* AppDelegate.swift in Sources */,
@@ -1236,6 +1392,8 @@
5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */, 5F4541A621C4449E00994C13 /* ButtonCell.swift in Sources */,
5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */, 5F45419821C2D60500994C13 /* KeyValueCell.swift in Sources */,
6FBA103E21D6B6D70051C35F /* TunnelImporter.swift in Sources */, 6FBA103E21D6B6D70051C35F /* TunnelImporter.swift in Sources */,
6F9B8A8E223398610041B9C4 /* SSIDOptionDetailTableViewController.swift in Sources */,
6F19D30422402B8700A126F2 /* ConfirmationAlertPresenter.swift in Sources */,
6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */, 6F919EC3218A2AE90023B400 /* ErrorPresenter.swift in Sources */,
6B62E45F220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */, 6B62E45F220A6FA900EF34A6 /* PrivateDataConfirmation.swift in Sources */,
6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */, 6F5A2B4821AFF49A0081EDD8 /* FileManager+Extension.swift in Sources */,
@@ -1244,7 +1402,9 @@
6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */,
6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */, 6F7774E82172020C006A79B3 /* TunnelConfiguration.swift in Sources */,
6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */,
6F29A9432278518D00DC6A6B /* RecentTunnelsTracker.swift in Sources */,
6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */, 6F6899A8218044FC0012E523 /* Curve25519.swift in Sources */,
6F0F44C9222D55BB00B0FF04 /* TextCell.swift in Sources */,
5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */, 5F4541A021C2D6B700994C13 /* TunnelListCell.swift in Sources */,
5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */, 5F9696B021CD7128008063FE /* TunnelConfiguration+WgQuickConfig.swift in Sources */,
6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */, 6F628C41217F47DB003482A3 /* TunnelDetailTableViewController.swift in Sources */,
@@ -1260,8 +1420,9 @@
5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */, 5F4541A221C2D6DF00994C13 /* BorderedTextButton.swift in Sources */,
5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */, 5FF7B96521CC95FA00A7DD74 /* PeerConfiguration.swift in Sources */,
6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */, 6F7774E1217181B1006A79B3 /* MainViewController.swift in Sources */,
6FFA5DA42197085D0001E2F7 /* ActivateOnDemandSetting.swift in Sources */, 6FFA5DA42197085D0001E2F7 /* ActivateOnDemandOption.swift in Sources */,
5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */, 5F4541B221CBFAEE00994C13 /* String+ArrayConversion.swift in Sources */,
6F907C9C224663A2003CED21 /* LogViewHelper.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1273,6 +1434,11 @@
target = 6F5D0C19218352EF000F85AD /* WireGuardNetworkExtensioniOS */; target = 6F5D0C19218352EF000F85AD /* WireGuardNetworkExtensioniOS */;
targetProxy = 6F5D0C20218352EF000F85AD /* PBXContainerItemProxy */; targetProxy = 6F5D0C20218352EF000F85AD /* PBXContainerItemProxy */;
}; };
6F70E23B22109DD3008BDFB4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 6F70E22822106A2D008BDFB4 /* WireGuardmacOSLoginItemHelper */;
targetProxy = 6F70E23A22109DD3008BDFB4 /* PBXContainerItemProxy */;
};
6FB1BD9821D4BFE700A991BF /* PBXTargetDependency */ = { 6FB1BD9821D4BFE700A991BF /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 6FB1BD9021D4BFE600A991BF /* WireGuardNetworkExtensionmacOS */; target = 6FB1BD9021D4BFE600A991BF /* WireGuardNetworkExtensionmacOS */;
@@ -1291,6 +1457,14 @@
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
6F70E20C221058DF008BDFB4 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
6F70E20D221058DF008BDFB4 /* Base */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
6FE1765421C90BBE002690EA /* Localizable.strings */ = { 6FE1765421C90BBE002690EA /* Localizable.strings */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@@ -1328,7 +1502,7 @@
PRODUCT_NAME = WireGuardNetworkExtension; PRODUCT_NAME = WireGuardNetworkExtension;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h";
SWIFT_VERSION = 4.2; SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
}; };
@@ -1350,7 +1524,33 @@
PRODUCT_NAME = WireGuardNetworkExtension; PRODUCT_NAME = WireGuardNetworkExtension;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h";
SWIFT_VERSION = 4.2; SWIFT_VERSION = 5.0;
};
name = Release;
};
6F70E23422106A31008BDFB4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = WireGuard/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
INFOPLIST_FILE = WireGuard/UI/macOS/LoginItemHelper/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS).login-item-helper";
PRODUCT_NAME = WireGuardLoginItemHelper;
SDKROOT = macosx;
SKIP_INSTALL = YES;
};
name = Debug;
};
6F70E23522106A31008BDFB4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = WireGuard/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements;
CODE_SIGN_IDENTITY = "Mac Developer";
INFOPLIST_FILE = WireGuard/UI/macOS/LoginItemHelper/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS).login-item-helper";
PRODUCT_NAME = WireGuardLoginItemHelper;
SDKROOT = macosx;
SKIP_INSTALL = YES;
}; };
name = Release; name = Release;
}; };
@@ -1370,6 +1570,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS)";
PRODUCT_NAME = WireGuard; PRODUCT_NAME = WireGuard;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
}; };
@@ -1389,6 +1590,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_MACOS)";
PRODUCT_NAME = WireGuard; PRODUCT_NAME = WireGuard;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_VERSION = 5.0;
}; };
name = Release; name = Release;
}; };
@@ -1410,6 +1612,7 @@
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h";
SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
}; };
@@ -1431,6 +1634,7 @@
SDKROOT = macosx; SDKROOT = macosx;
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "WireGuardNetworkExtension/WireGuardNetworkExtension-Bridging-Header.h";
SWIFT_VERSION = 5.0;
}; };
name = Release; name = Release;
}; };
@@ -1503,6 +1707,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@@ -1568,6 +1773,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -1607,6 +1813,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_IOS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_IOS)";
PRODUCT_NAME = WireGuard; PRODUCT_NAME = WireGuard;
SWIFT_VERSION = 5.0;
}; };
name = Debug; name = Debug;
}; };
@@ -1623,6 +1830,7 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_IOS)"; PRODUCT_BUNDLE_IDENTIFIER = "$(APP_ID_IOS)";
PRODUCT_NAME = WireGuard; PRODUCT_NAME = WireGuard;
SWIFT_VERSION = 5.0;
}; };
name = Release; name = Release;
}; };
@@ -1638,6 +1846,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
6F70E23622106A31008BDFB4 /* Build configuration list for PBXNativeTarget "WireGuardmacOSLoginItemHelper" */ = {
isa = XCConfigurationList;
buildConfigurations = (
6F70E23422106A31008BDFB4 /* Debug */,
6F70E23522106A31008BDFB4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
6FB1BD6A21D2607E00A991BF /* Build configuration list for PBXNativeTarget "WireGuardmacOS" */ = { 6FB1BD6A21D2607E00A991BF /* Build configuration list for PBXNativeTarget "WireGuardmacOS" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
@@ -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";
@@ -13,6 +13,10 @@
"tunnelsListSettingsButtonTitle" = "Settings"; "tunnelsListSettingsButtonTitle" = "Settings";
"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel"; "tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
"tunnelsListSwipeDeleteButtonTitle" = "Delete"; "tunnelsListSwipeDeleteButtonTitle" = "Delete";
"tunnelsListSelectButtonTitle" = "Select";
"tunnelsListSelectAllButtonTitle" = "Select All";
"tunnelsListDeleteButtonTitle" = "Delete";
"tunnelsListSelectedTitle (%d)" = "%d selected";
// Tunnels list menu // Tunnels list menu
@@ -23,11 +27,18 @@
// Tunnels list alerts // 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"; "alertImportedFromZipTitle (%d)" = "Created %d tunnels";
"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive"; "alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
"alertUnableToImportTitle" = "Unable to import tunnel"; "alertBadConfigImportTitle" = "Unable to import tunnel";
"alertUnableToImportMessage" = "An error occured when importing the tunnel configuration."; "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 // Tunnel detail and edit UI
@@ -44,6 +55,14 @@
"tunnelStatusRestarting" = "Restarting"; "tunnelStatusRestarting" = "Restarting";
"tunnelStatusWaiting" = "Waiting"; "tunnelStatusWaiting" = "Waiting";
"macToggleStatusButtonActivate" = "Activate";
"macToggleStatusButtonActivating" = "Activating…";
"macToggleStatusButtonDeactivate" = "Deactivate";
"macToggleStatusButtonDeactivating" = "Deactivating…";
"macToggleStatusButtonReasserting" = "Reactivating…";
"macToggleStatusButtonRestarting" = "Restarting…";
"macToggleStatusButtonWaiting" = "Waiting…";
"tunnelSectionTitleInterface" = "Interface"; "tunnelSectionTitleInterface" = "Interface";
"tunnelInterfaceName" = "Name"; "tunnelInterfaceName" = "Name";
@@ -54,6 +73,7 @@
"tunnelInterfaceListenPort" = "Listen port"; "tunnelInterfaceListenPort" = "Listen port";
"tunnelInterfaceMTU" = "MTU"; "tunnelInterfaceMTU" = "MTU";
"tunnelInterfaceDNS" = "DNS servers"; "tunnelInterfaceDNS" = "DNS servers";
"tunnelInterfaceStatus" = "Status";
"tunnelSectionTitlePeer" = "Peer"; "tunnelSectionTitlePeer" = "Peer";
@@ -69,7 +89,28 @@
"tunnelSectionTitleOnDemand" = "On-Demand Activation"; "tunnelSectionTitleOnDemand" = "On-Demand Activation";
"tunnelOnDemandKey" = "Activate on demand"; "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"; "tunnelOnDemandOptionOff" = "Off";
"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only"; "tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular"; "tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
@@ -111,6 +152,8 @@
"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours"; "tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours";
"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes"; "tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes";
"tunnelPeerPresharedKeyEnabled" = "enabled";
// Error alerts while creating / editing a tunnel configuration // Error alerts while creating / editing a tunnel configuration
/* Alert title for error in the interface data */ /* Alert title for error in the interface data */
@@ -166,17 +209,18 @@
"settingsSectionTitleExportConfigurations" = "Export configurations"; "settingsSectionTitleExportConfigurations" = "Export configurations";
"settingsExportZipButtonTitle" = "Export zip archive"; "settingsExportZipButtonTitle" = "Export zip archive";
"settingsSectionTitleTunnelLog" = "Tunnel log"; "settingsSectionTitleTunnelLog" = "Log";
"settingsExportLogFileButtonTitle" = "Export log file"; "settingsViewLogButtonTitle" = "View log";
// Settings alerts // Log view
"logViewTitle" = "Log";
// Log alerts
"alertUnableToRemovePreviousLogTitle" = "Log export failed"; "alertUnableToRemovePreviousLogTitle" = "Log export failed";
"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared"; "alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
"alertUnableToFindExtensionLogPathTitle" = "Log export failed";
"alertUnableToFindExtensionLogPathMessage" = "Unable to determine extension log path";
"alertUnableToWriteLogTitle" = "Log export failed"; "alertUnableToWriteLogTitle" = "Log export failed";
"alertUnableToWriteLogMessage" = "Unable to write logs to file"; "alertUnableToWriteLogMessage" = "Unable to write logs to file";
@@ -197,6 +241,11 @@
"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive"; "alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the 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 // Tunnel management error alerts
"alertTunnelActivationFailureTitle" = "Activation failure"; "alertTunnelActivationFailureTitle" = "Activation failure";
@@ -242,42 +291,69 @@
"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed."; "alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error."; "alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
// Mac status bar menu / pulldown menu // Mac status bar menu / pulldown menu / main menu
"macMenuNetworks (%@)" = "Networks: %@"; "macMenuNetworks (%@)" = "Networks: %@";
"macMenuNetworksNone" = "Networks: None"; "macMenuNetworksNone" = "Networks: None";
"macMenuTitle" = "WireGuard"; "macMenuTitle" = "WireGuard";
"macMenuManageTunnels" = "Manage tunnels"; "macMenuManageTunnels" = "Manage Tunnels";
"macMenuImportTunnels" = "Import tunnel(s) from file…"; "macMenuImportTunnels" = "Import Tunnel(s) from File…";
"macMenuAddEmptyTunnel" = "Add empty tunnel…"; "macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
"macMenuExportLog" = "Export log to file…"; "macMenuViewLog" = "View Log";
"macMenuExportTunnels" = "Export tunnels to zip…"; "macMenuExportTunnels" = "Export Tunnels to Zip…";
"macMenuAbout" = "About WireGuard"; "macMenuAbout" = "About WireGuard";
"macMenuQuit" = "Quit"; "macMenuQuit" = "Quit WireGuard";
"macMenuHideApp" = "Hide WireGuard";
"macMenuHideOtherApps" = "Hide Others";
"macMenuShowAllApps" = "Show All";
"macMenuFile" = "File";
"macMenuCloseWindow" = "Close Window";
"macMenuEdit" = "Edit";
"macMenuCut" = "Cut";
"macMenuCopy" = "Copy";
"macMenuPaste" = "Paste";
"macMenuSelectAll" = "Select All";
"macMenuTunnel" = "Tunnel";
"macMenuToggleStatus" = "Toggle Status";
"macMenuEditTunnel" = "Edit…";
"macMenuDeleteSelected" = "Delete Selected";
"macMenuWindow" = "Window";
"macMenuMinimize" = "Minimize";
"macMenuZoom" = "Zoom";
// Mac manage tunnels window // Mac manage tunnels window
"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels"; "macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
"macDeleteTunnelConfirmationAlertMessage (%@)" = "Are you sure you want to delete %@?"; "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."; "macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete"; "macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel"; "macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Deleting…";
"macButtonImportTunnels" = "Import tunnel(s) from file"; "macButtonImportTunnels" = "Import tunnel(s) from file";
"macSheetButtonImport" = "Import"; "macSheetButtonImport" = "Import";
"macNameFieldExportLog" = "Export log to"; "macNameFieldExportLog" = "Save log to:";
"macSheetButtonExportLog" = "Save"; "macSheetButtonExportLog" = "Save";
"macNameFieldExportZip" = "Export tunnels to"; "macNameFieldExportZip" = "Export tunnels to:";
"macSheetButtonExportZip" = "Save"; "macSheetButtonExportZip" = "Save";
"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
// Mac detail/edit view fields // Mac detail/edit view fields
"macFieldKey (%@)" = "%@:"; "macFieldKey (%@)" = "%@:";
"macFieldOnDemand" = "On-Demand:"; "macFieldOnDemand" = "On-Demand:";
"macFieldOnDemandSSIDs" = "SSIDs:";
// Mac status display // Mac status display
@@ -329,5 +405,39 @@
// Mac alert // Mac alert
"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.\n\nNote that if you quit WireGuard entirely the currently active tunnel ('%@') will still remain active until you deactivate it from this application or through the Network panel in System Preferences.";
"macConfirmAndQuitAlertQuitWireGuard" = "Quit WireGuard";
"macConfirmAndQuitAlertCloseWindow" = "Close Tunnels Manager";
"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel"; "macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences."; "macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
// Mac tooltip
"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
"macToolTipToggleStatus" = "Toggle status (⌘T)";
// Mac log view
"macLogColumnTitleTime" = "Time";
"macLogColumnTitleLogMessage" = "Log message";
"macLogButtonTitleClose" = "Close";
"macLogButtonTitleSave" = "Save…";
// Mac unusable tunnel view
"macUnusableTunnelMessage" = "The configuration for this tunnel cannot be found in the keychain.";
"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
"macUnusableTunnelButtonTitleDeleteTunnel" = "Delete tunnel";
// Mac App Store updating alert
"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel %@, deactivate it, and then continue updating in App Store.";
"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel %@ and then continue updating in App Store.";
// Donation
"donateLink" = "♥ Donate to the WireGuard Project";
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME = 0.0.20190207 VERSION_NAME = 0.0.20191015
VERSION_ID = 2 VERSION_ID = 15
+3 -3
View File
@@ -9,7 +9,7 @@ struct Curve25519 {
static func generatePrivateKey() -> Data { static func generatePrivateKey() -> Data {
var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength) var privateKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeMutableBytes { bytes in privateKey.withUnsafeMutableUInt8Bytes { bytes in
curve25519_generate_private_key(bytes) curve25519_generate_private_key(bytes)
} }
assert(privateKey.count == TunnelConfiguration.keyLength) assert(privateKey.count == TunnelConfiguration.keyLength)
@@ -19,8 +19,8 @@ struct Curve25519 {
static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data { static func generatePublicKey(fromPrivateKey privateKey: Data) -> Data {
assert(privateKey.count == TunnelConfiguration.keyLength) assert(privateKey.count == TunnelConfiguration.keyLength)
var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength) var publicKey = Data(repeating: 0, count: TunnelConfiguration.keyLength)
privateKey.withUnsafeBytes { privateKeyBytes in privateKey.withUnsafeUInt8Bytes { privateKeyBytes in
publicKey.withUnsafeMutableBytes { bytes in publicKey.withUnsafeMutableUInt8Bytes { bytes in
curve25519_derive_public_key(bytes, privateKeyBytes) curve25519_derive_public_key(bytes, privateKeyBytes)
} }
} }
@@ -0,0 +1,133 @@
// 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) {
if tunnelProviderManager.isOnDemandEnabled, let onDemandRules = tunnelProviderManager.onDemandRules {
self = ActivateOnDemandOption.create(from: onDemandRules)
} else {
self = .off
}
}
private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
switch rules.count {
case 0:
return .off
case 1:
let rule = rules[0]
guard rule.action == .connect else { return .off }
return .anyInterface(.anySSID)
case 2:
guard let connectRule = rules.first(where: { $0.action == .connect }) else {
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
return .off
}
guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
return .off
}
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
return .wiFiInterfaceOnly(.anySSID)
} else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
return .nonWiFiInterfaceOnly
} else {
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
return .off
}
case 3:
guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
let ssids = ssidRule.ssidMatch!
switch (ssidRule.action, nonWiFiRule.action) {
case (.connect, .connect):
return .anyInterface(.onlySpecificSSIDs(ssids))
case (.connect, .disconnect):
return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
case (.disconnect, .connect):
return .anyInterface(.exceptSpecificSSIDs(ssids))
case (.disconnect, .disconnect):
return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
default:
wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
return .off
}
default:
wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
return .off
}
}
}
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)]
}
}
@@ -1,109 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import NetworkExtension
struct ActivateOnDemandSetting {
var isActivateOnDemandEnabled: Bool
var activateOnDemandOption: ActivateOnDemandOption
}
enum ActivateOnDemandOption {
case none // Valid only when isActivateOnDemandEnabled is false
case useOnDemandOverWiFiOnly
#if os(iOS)
case useOnDemandOverWiFiOrCellular
case useOnDemandOverCellularOnly
#elseif os(macOS)
case useOnDemandOverWiFiOrEthernet
case useOnDemandOverEthernetOnly
#else
#error("Unimplemented")
#endif
}
extension ActivateOnDemandSetting {
func apply(on tunnelProviderManager: NETunnelProviderManager) {
tunnelProviderManager.isOnDemandEnabled = isActivateOnDemandEnabled
let rules: [NEOnDemandRule]?
let connectRule = NEOnDemandRuleConnect()
let disconnectRule = NEOnDemandRuleDisconnect()
switch activateOnDemandOption {
case .none:
rules = nil
#if os(iOS)
case .useOnDemandOverWiFiOrCellular:
rules = [connectRule]
case .useOnDemandOverWiFiOnly:
connectRule.interfaceTypeMatch = .wiFi
disconnectRule.interfaceTypeMatch = .cellular
rules = [connectRule, disconnectRule]
case .useOnDemandOverCellularOnly:
connectRule.interfaceTypeMatch = .cellular
disconnectRule.interfaceTypeMatch = .wiFi
rules = [connectRule, disconnectRule]
#elseif os(macOS)
case .useOnDemandOverWiFiOrEthernet:
rules = [connectRule]
case .useOnDemandOverWiFiOnly:
connectRule.interfaceTypeMatch = .wiFi
disconnectRule.interfaceTypeMatch = .ethernet
rules = [connectRule, disconnectRule]
case .useOnDemandOverEthernetOnly:
connectRule.interfaceTypeMatch = .ethernet
disconnectRule.interfaceTypeMatch = .wiFi
rules = [connectRule, disconnectRule]
#else
#error("Unimplemented")
#endif
}
tunnelProviderManager.onDemandRules = rules
}
init(from tunnelProviderManager: NETunnelProviderManager) {
let rules = tunnelProviderManager.onDemandRules ?? []
#if os(iOS)
let otherInterfaceType: NEOnDemandRuleInterfaceType = .cellular
let useWiFiOrOtherOption: ActivateOnDemandOption = .useOnDemandOverWiFiOrCellular
let useOtherOnlyOption: ActivateOnDemandOption = .useOnDemandOverCellularOnly
#elseif os(macOS)
let otherInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
let useWiFiOrOtherOption: ActivateOnDemandOption = .useOnDemandOverWiFiOrEthernet
let useOtherOnlyOption: ActivateOnDemandOption = .useOnDemandOverEthernetOnly
#else
#error("Unimplemented")
#endif
let activateOnDemandOption: ActivateOnDemandOption
switch rules.count {
case 0:
activateOnDemandOption = .none
case 1:
let rule = rules[0]
precondition(rule.action == .connect)
activateOnDemandOption = useWiFiOrOtherOption
case 2:
let connectRule = rules.first(where: { $0.action == .connect })!
let disconnectRule = rules.first(where: { $0.action == .disconnect })!
if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == otherInterfaceType {
activateOnDemandOption = .useOnDemandOverWiFiOnly
} else if connectRule.interfaceTypeMatch == otherInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
activateOnDemandOption = useOtherOnlyOption
} else {
fatalError("Unexpected onDemandRules set on tunnel provider manager")
}
default:
fatalError("Unexpected number of onDemandRules set on tunnel provider manager")
}
self.activateOnDemandOption = activateOnDemandOption
if activateOnDemandOption == .none {
isActivateOnDemandEnabled = false
} else {
isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
}
}
}
extension ActivateOnDemandSetting {
static var defaultSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: false, activateOnDemandOption: .none)
}
@@ -27,6 +27,8 @@ import NetworkExtension
self = .reasserting self = .reasserting
case .invalid: case .invalid:
self = .inactive self = .inactive
@unknown default:
fatalError()
} }
} }
} }
@@ -54,6 +56,8 @@ extension NEVPNStatus: CustomDebugStringConvertible {
case .disconnecting: return "disconnecting" case .disconnecting: return "disconnecting"
case .reasserting: return "reasserting" case .reasserting: return "reasserting"
case .invalid: return "invalid" case .invalid: return "invalid"
@unknown default:
fatalError()
} }
} }
} }
+145 -38
View File
@@ -28,12 +28,12 @@ class TunnelsManager {
private var configurationsObservationToken: AnyObject? private var configurationsObservationToken: AnyObject?
init(tunnelProviders: [NETunnelProviderManager]) { init(tunnelProviders: [NETunnelProviderManager]) {
tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { $0.name < $1.name } tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
startObservingTunnelStatuses() startObservingTunnelStatuses()
startObservingTunnelConfigurations() startObservingTunnelConfigurations()
} }
static func create(completionHandler: @escaping (WireGuardResult<TunnelsManager>) -> Void) { static func create(completionHandler: @escaping (Result<TunnelsManager, TunnelsManagerError>) -> Void) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels()))) completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
#else #else
@@ -46,19 +46,39 @@ class TunnelsManager {
var tunnelManagers = managers ?? [] var tunnelManagers = managers ?? []
var refs: Set<Data> = [] var refs: Set<Data> = []
var tunnelNames: Set<String> = []
for (index, tunnelManager) in tunnelManagers.enumerated().reversed() { for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol if let tunnelName = tunnelManager.localizedDescription {
if proto?.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") ?? false { tunnelNames.insert(tunnelName)
}
guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue }
if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") {
tunnelManager.saveToPreferences { _ in } tunnelManager.saveToPreferences { _ in }
} }
if let ref = proto?.verifyConfigurationReference() { #if os(iOS)
let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
#elseif os(macOS)
let passwordRef: Data?
if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
} else {
passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
}
#else
#error("Unimplemented")
#endif
if let ref = passwordRef {
refs.insert(ref) refs.insert(ref)
} else { } else {
wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
tunnelManager.removeFromPreferences { _ in } tunnelManager.removeFromPreferences { _ in }
tunnelManagers.remove(at: index) tunnelManagers.remove(at: index)
} }
} }
Keychain.deleteReferences(except: refs) Keychain.deleteReferences(except: refs)
#if os(iOS)
RecentTunnelsTracker.cleanupTunnels(except: tunnelNames)
#endif
completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers))) completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
} }
#endif #endif
@@ -71,28 +91,33 @@ class TunnelsManager {
let loadedTunnelProviders = managers ?? [] let loadedTunnelProviders = managers ?? []
for (index, currentTunnel) in self.tunnels.enumerated().reversed() { for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
if !loadedTunnelProviders.contains(where: { $0.tunnelConfiguration == currentTunnel.tunnelConfiguration }) { if !loadedTunnelProviders.contains(where: { $0.isEquivalentTo(currentTunnel) }) {
// Tunnel was deleted outside the app // Tunnel was deleted outside the app
self.tunnels.remove(at: index) self.tunnels.remove(at: index)
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel) self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
} }
} }
for loadedTunnelProvider in loadedTunnelProviders { for loadedTunnelProvider in loadedTunnelProviders {
if let matchingTunnel = self.tunnels.first(where: { $0.tunnelConfiguration == loadedTunnelProvider.tunnelConfiguration }) { if let matchingTunnel = self.tunnels.first(where: { loadedTunnelProvider.isEquivalentTo($0) }) {
matchingTunnel.tunnelProvider = loadedTunnelProvider matchingTunnel.tunnelProvider = loadedTunnelProvider
matchingTunnel.refreshStatus() matchingTunnel.refreshStatus()
} else { } else {
// Tunnel was added outside the app // 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) let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
self.tunnels.append(tunnel) self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name } self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
} }
} }
} }
} }
func add(tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting = ActivateOnDemandSetting.defaultSetting, completionHandler: @escaping (WireGuardResult<TunnelContainer>) -> Void) { func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, completionHandler: @escaping (Result<TunnelContainer, TunnelsManagerError>) -> Void) {
let tunnelName = tunnelConfiguration.name ?? "" let tunnelName = tunnelConfiguration.name ?? ""
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty)) completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
@@ -108,7 +133,9 @@ class TunnelsManager {
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration) tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
activateOnDemandSetting.apply(on: tunnelProviderManager) onDemandOption.apply(on: tunnelProviderManager)
let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
@@ -120,32 +147,53 @@ class TunnelsManager {
guard let self = self else { 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) let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
self.tunnels.append(tunnel) self.tunnels.append(tunnel)
self.tunnels.sort { $0.name < $1.name } self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
completionHandler(.success(tunnel)) completionHandler(.success(tunnel))
} }
} }
func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt) -> Void) { func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, completionHandler: completionHandler) addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
} }
private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, completionHandler: @escaping (UInt) -> Void) { private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
guard let head = tunnelConfigurations.first else { guard let head = tunnelConfigurations.first else {
completionHandler(numberSuccessful) completionHandler(numberSuccessful, lastError)
return return
} }
let tail = tunnelConfigurations.dropFirst() let tail = tunnelConfigurations.dropFirst()
add(tunnelConfiguration: head) { [weak self, tail] result in add(tunnelConfiguration: head) { [weak self, tail] result in
DispatchQueue.main.async { DispatchQueue.main.async {
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessful + (result.isSuccess ? 1 : 0), completionHandler: completionHandler) var numberSuccessfulCount = numberSuccessful
var lastError: TunnelsManagerError?
switch result {
case .failure(let error):
lastError = error
case .success:
numberSuccessfulCount = numberSuccessful + 1
}
self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessfulCount, lastError: lastError, completionHandler: completionHandler)
} }
} }
} }
func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, activateOnDemandSetting: ActivateOnDemandSetting, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelName = tunnelConfiguration.name ?? "" let tunnelName = tunnelConfiguration.name ?? ""
if tunnelName.isEmpty { if tunnelName.isEmpty {
completionHandler(TunnelsManagerError.tunnelNameEmpty) completionHandler(TunnelsManagerError.tunnelNameEmpty)
@@ -153,7 +201,8 @@ class TunnelsManager {
} }
let tunnelProviderManager = tunnel.tunnelProvider let tunnelProviderManager = tunnel.tunnelProvider
let isNameChanged = tunnelName != tunnelProviderManager.localizedDescription let oldName = tunnelProviderManager.localizedDescription ?? ""
let isNameChanged = tunnelName != oldName
if isNameChanged { if isNameChanged {
guard !tunnels.contains(where: { $0.name == tunnelName }) else { guard !tunnels.contains(where: { $0.name == tunnelName }) else {
completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName) completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
@@ -162,11 +211,15 @@ class TunnelsManager {
tunnel.name = tunnelName tunnel.name = tunnelName
} }
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration) var isTunnelConfigurationChanged = false
if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
isTunnelConfigurationChanged = true
}
tunnelProviderManager.isEnabled = true tunnelProviderManager.isEnabled = true
let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && activateOnDemandSetting.isActivateOnDemandEnabled let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
activateOnDemandSetting.apply(on: tunnelProviderManager) onDemandOption.apply(on: tunnelProviderManager)
tunnelProviderManager.saveToPreferences { [weak self] error in tunnelProviderManager.saveToPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
@@ -178,16 +231,21 @@ class TunnelsManager {
guard let self = self else { return } guard let self = self else { return }
if isNameChanged { if isNameChanged {
let oldIndex = self.tunnels.firstIndex(of: tunnel)! let oldIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.sort { $0.name < $1.name } self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
let newIndex = self.tunnels.firstIndex(of: tunnel)! let newIndex = self.tunnels.firstIndex(of: tunnel)!
self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex) self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
#if os(iOS)
RecentTunnelsTracker.handleTunnelRenamed(oldName: oldName, newName: tunnelName)
#endif
} }
self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!) self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting { if isTunnelConfigurationChanged {
// Turn off the tunnel, and then turn it back on, so the changes are made effective if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
tunnel.status = .restarting // Turn off the tunnel, and then turn it back on, so the changes are made effective
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel() tunnel.status = .restarting
(tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
}
} }
if isActivatingOnDemand { if isActivatingOnDemand {
@@ -210,20 +268,51 @@ class TunnelsManager {
func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) { func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
let tunnelProviderManager = tunnel.tunnelProvider let tunnelProviderManager = tunnel.tunnelProvider
#if os(macOS)
if tunnel.isTunnelAvailableToUser {
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
}
#elseif os(iOS)
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference() (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
#else
#error("Unimplemented")
#endif
tunnelProviderManager.removeFromPreferences { [weak self] error in tunnelProviderManager.removeFromPreferences { [weak self] error in
guard error == nil else { guard error == nil else {
wg_log(.error, message: "Remove: Saving configuration failed: \(error!)") wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!)) completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
return return
} }
if let self = self { if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
let index = self.tunnels.firstIndex(of: tunnel)!
self.tunnels.remove(at: index) self.tunnels.remove(at: index)
self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel) self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
} }
completionHandler(nil) completionHandler(nil)
#if os(iOS)
RecentTunnelsTracker.handleTunnelRemoved(tunnelName: tunnel.name)
#endif
}
}
func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
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)
}
}
} }
} }
@@ -235,6 +324,10 @@ class TunnelsManager {
return tunnels[index] return tunnels[index]
} }
func mapTunnels<T>(transform: (TunnelContainer) throws -> T) rethrows -> [T] {
return try tunnels.map(transform)
}
func index(of tunnel: TunnelContainer) -> Int? { func index(of tunnel: TunnelContainer) -> Int? {
return tunnels.firstIndex(of: tunnel) return tunnels.firstIndex(of: tunnel)
} }
@@ -280,6 +373,10 @@ class TunnelsManager {
#else #else
tunnel.startActivation(activationDelegate: activationDelegate) tunnel.startActivation(activationDelegate: activationDelegate)
#endif #endif
#if os(iOS)
RecentTunnelsTracker.handleTunnelActivated(tunnelName: tunnel.name)
#endif
} }
func startDeactivation(of tunnel: TunnelContainer) { func startDeactivation(of tunnel: TunnelContainer) {
@@ -313,11 +410,7 @@ class TunnelsManager {
guard let self = self, guard let self = self,
let session = statusChangeNotification.object as? NETunnelProviderSession, let session = statusChangeNotification.object as? NETunnelProviderSession,
let tunnelProvider = session.manager as? NETunnelProviderManager, let tunnelProvider = session.manager as? NETunnelProviderManager,
let tunnelConfiguration = tunnelProvider.tunnelConfiguration, let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
let tunnel = self.tunnels.first(where: { $0.tunnelConfiguration == tunnelConfiguration }) else { return }
if tunnel.tunnelProvider != tunnelProvider {
return
}
wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'") wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
@@ -356,6 +449,9 @@ class TunnelsManager {
} }
} }
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)? { private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
@@ -408,10 +504,16 @@ class TunnelContainer: NSObject {
return tunnelProvider.tunnelConfiguration return tunnelProvider.tunnelConfiguration
} }
var activateOnDemandSetting: ActivateOnDemandSetting { var onDemandOption: ActivateOnDemandOption {
return ActivateOnDemandSetting(from: tunnelProvider) return ActivateOnDemandOption(from: tunnelProvider)
} }
#if os(macOS)
var isTunnelAvailableToUser: Bool {
return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
}
#endif
init(tunnel: NETunnelProviderManager) { init(tunnel: NETunnelProviderManager) {
name = tunnel.localizedDescription ?? "Unnamed" name = tunnel.localizedDescription ?? "Unnamed"
let status = TunnelStatus(from: tunnel.connection.status) let status = TunnelStatus(from: tunnel.connection.status)
@@ -426,7 +528,7 @@ class TunnelContainer: NSObject {
completionHandler(tunnelConfiguration) completionHandler(tunnelConfiguration)
return return
} }
guard nil != (try? session.sendProviderMessage(Data(bytes: [ 0 ]), responseHandler: { guard nil != (try? session.sendProviderMessage(Data([ UInt8(0) ]), responseHandler: {
guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else { guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
completionHandler(self.tunnelConfiguration) completionHandler(self.tunnelConfiguration)
return return
@@ -439,7 +541,7 @@ class TunnelContainer: NSObject {
} }
func refreshStatus() { func refreshStatus() {
if (status == .restarting) && (tunnelProvider.connection.status == .disconnected || tunnelProvider.connection.status == .disconnecting) { if status == .restarting {
return return
} }
status = TunnelStatus(from: tunnelProvider.connection.status) status = TunnelStatus(from: tunnelProvider.connection.status)
@@ -521,6 +623,7 @@ class TunnelContainer: NSObject {
extension NETunnelProviderManager { extension NETunnelProviderManager {
private static var cachedConfigKey: UInt8 = 0 private static var cachedConfigKey: UInt8 = 0
var tunnelConfiguration: TunnelConfiguration? { var tunnelConfiguration: TunnelConfiguration? {
if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration { if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
return cached return cached
@@ -537,4 +640,8 @@ extension NETunnelProviderManager {
localizedDescription = tunnelConfiguration.name localizedDescription = tunnelConfiguration.name
objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
} }
func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
}
} }
@@ -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
}
}
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
public class LogViewHelper {
var log: OpaquePointer
var cursor: UInt32 = UINT32_MAX
static let formatOptions: ISO8601DateFormatter.Options = [
.withYear, .withMonth, .withDay, .withTime,
.withDashSeparatorInDate, .withColonSeparatorInTime, .withSpaceBetweenDateAndTime,
.withFractionalSeconds
]
struct LogEntry {
let timestamp: String
let message: String
func text() -> String {
return timestamp + " " + message
}
}
class LogEntries {
var entries: [LogEntry] = []
}
init?(logFilePath: String?) {
guard let logFilePath = logFilePath else { return nil }
guard let log = open_log(logFilePath) else { return nil }
self.log = log
}
deinit {
close_log(self.log)
}
func fetchLogEntriesSinceLastFetch(completion: @escaping ([LogViewHelper.LogEntry]) -> Void) {
var logEntries = LogEntries()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
let newCursor = view_lines_from_cursor(self.log, self.cursor, &logEntries) { cStr, timestamp, ctx in
let message = cStr != nil ? String(cString: cStr!) : ""
let date = Date(timeIntervalSince1970: Double(timestamp) / 1000000000)
let dateString = ISO8601DateFormatter.string(from: date, timeZone: TimeZone.current, formatOptions: LogViewHelper.formatOptions)
if let logEntries = ctx?.bindMemory(to: LogEntries.self, capacity: 1) {
logEntries.pointee.entries.append(LogEntry(timestamp: dateString, message: message))
}
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.cursor = newCursor
completion(logEntries.entries)
}
}
}
}
+79 -28
View File
@@ -4,40 +4,91 @@
import Foundation import Foundation
class TunnelImporter { class TunnelImporter {
static func importFromFile(url: URL, into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) { static func importFromFile(urls: [URL], into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) {
if url.pathExtension == "zip" { guard !urls.isEmpty else {
ZipImporter.importConfigFiles(from: url) { result in completionHandler?()
if let error = result.error { return
errorPresenterType.showErrorAlert(error: error, from: sourceVC) }
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
switch result {
case .failure(let error):
lastFileImportErrorText = error.alertText
case .success(let configsInZip):
configs.append(contentsOf: configsInZip)
}
dispatchGroup.leave()
} }
let configs = result.value! } else { /* if it is not a zip, we assume it is a conf */
tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { numberSuccessful in let fileName = url.lastPathComponent
if numberSuccessful == configs.count { let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
completionHandler?() 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 return
} }
let title = tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful) var parseError: Error?
let message = tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count) var tunnelConfiguration: TunnelConfiguration?
errorPresenterType.showErrorAlert(title: title, message: message, from: sourceVC, onPresented: completionHandler) do {
} tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
} } catch let error {
} else /* if (url.pathExtension == "conf") -- we assume everything else is a conf */ { parseError = error
let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) }
if let fileContents = try? String(contentsOf: url), DispatchQueue.main.async {
let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName) { if parseError != nil {
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration) { result in if let parseError = parseError as? WireGuardAppError {
if let error = result.error { lastFileImportErrorText = parseError.alertText
errorPresenterType.showErrorAlert(error: error, from: sourceVC, onPresented: completionHandler) } else {
} else { lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
completionHandler?() }
}
configs.append(tunnelConfiguration)
dispatchGroup.leave()
} }
} }
} else { }
errorPresenterType.showErrorAlert(title: tr("alertUnableToImportTitle"), message: tr("alertUnableToImportMessage"), }
from: sourceVC, onPresented: completionHandler) 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?()
}
} }
} }
} }
} }
+78 -100
View File
@@ -14,6 +14,8 @@ class TunnelViewModel {
case listenPort case listenPort
case mtu case mtu
case dns case dns
case status
case toggleStatus
var localizedUIString: String { var localizedUIString: String {
switch self { switch self {
@@ -25,6 +27,8 @@ class TunnelViewModel {
case .listenPort: return tr("tunnelInterfaceListenPort") case .listenPort: return tr("tunnelInterfaceListenPort")
case .mtu: return tr("tunnelInterfaceMTU") case .mtu: return tr("tunnelInterfaceMTU")
case .dns: return tr("tunnelInterfaceDNS") case .dns: return tr("tunnelInterfaceDNS")
case .status: return tr("tunnelInterfaceStatus")
case .toggleStatus: return ""
} }
} }
} }
@@ -67,17 +71,17 @@ class TunnelViewModel {
static let keyLengthInBase64 = 44 static let keyLengthInBase64 = 44
struct ChangeHandlers { struct Changes {
enum FieldChange { enum FieldChange: Equatable {
case added case added
case removed case removed
case modified case modified(newValue: String)
} }
var interfaceChanged: ([InterfaceField: FieldChange]) -> Void var interfaceChanges: [InterfaceField: FieldChange]
var peerChangedAt: (Int, [PeerField: FieldChange]) -> Void var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
var peersRemovedAt: ([Int]) -> Void var peersRemovedIndices: [Int]
var peersInsertedAt: ([Int]) -> Void var peersInsertedIndices: [Int]
} }
class InterfaceData { class InterfaceData {
@@ -217,12 +221,12 @@ class TunnelViewModel {
} }
} }
func applyConfiguration(other: InterfaceConfiguration, otherName: String, changeHandler: ([InterfaceField: ChangeHandlers.FieldChange]) -> Void) { func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
if scratchpad.isEmpty { if scratchpad.isEmpty {
populateScratchpad() populateScratchpad()
} }
let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName) let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
var changes = [InterfaceField: ChangeHandlers.FieldChange]() var changes = [InterfaceField: Changes.FieldChange]()
for field in InterfaceField.allCases { for field in InterfaceField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""): case ("", ""):
@@ -233,14 +237,12 @@ class TunnelViewModel {
changes[field] = .removed changes[field] = .removed
case (let this, let other): case (let this, let other):
if this != other { if this != other {
changes[field] = .modified changes[field] = .modified(newValue: other)
} }
} }
} }
scratchpad = otherScratchPad scratchpad = otherScratchPad
if !changes.isEmpty { return changes
changeHandler(changes)
}
} }
} }
@@ -403,50 +405,60 @@ 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" "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
] ]
static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
guard isSinglePeer else {
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
}
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 {
return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
}
}
func updateExcludePrivateIPsFieldState() { func updateExcludePrivateIPsFieldState() {
if scratchpad.isEmpty { if scratchpad.isEmpty {
populateScratchpad() populateScratchpad()
} }
let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)) 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) shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
guard numberOfPeers == 1 else { }
shouldAllowExcludePrivateIPsControl = false
excludePrivateIPsValue = false static func normalizedIPAddressRangeStrings(_ list: [String]) -> [String] {
return return list.compactMap { IPAddressRange(from: $0) }.map { $0.stringRepresentation }
} }
if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
shouldAllowExcludePrivateIPsControl = true static func modifiedAllowedIPs(currentAllowedIPs: [String], excludePrivateIPs: Bool, dnsServers: [String], oldDNSServers: [String]?) -> [String] {
excludePrivateIPsValue = false let normalizedDNSServers = normalizedIPAddressRangeStrings(dnsServers)
} else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) { let normalizedOldDNSServers = oldDNSServers == nil ? normalizedDNSServers : normalizedIPAddressRangeStrings(oldDNSServers!)
shouldAllowExcludePrivateIPsControl = true let ipv6Addresses = normalizedIPAddressRangeStrings(currentAllowedIPs.filter { $0.contains(":") })
excludePrivateIPsValue = true if excludePrivateIPs {
return ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + normalizedDNSServers
} else { } else {
shouldAllowExcludePrivateIPsControl = false return ipv6Addresses.filter { !normalizedOldDNSServers.contains($0) } + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
excludePrivateIPsValue = false
} }
} }
func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String) { func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String, oldDNSServers: String? = nil) {
let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines) let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines) let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let ipv6Addresses = allowedIPStrings.filter { $0.contains(":") } let oldDNSServerStrings = oldDNSServers?.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let modifiedAllowedIPStrings: [String] let modifiedAllowedIPStrings = TunnelViewModel.PeerData.modifiedAllowedIPs(currentAllowedIPs: allowedIPStrings, excludePrivateIPs: isOn, dnsServers: dnsServerStrings, oldDNSServers: oldDNSServerStrings)
if isOn {
modifiedAllowedIPStrings = ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + dnsServerStrings
} else {
modifiedAllowedIPStrings = ipv6Addresses + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
}
scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ") scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
validatedConfiguration = nil validatedConfiguration = nil
excludePrivateIPsValue = isOn excludePrivateIPsValue = isOn
} }
func applyConfiguration(other: PeerConfiguration, peerIndex: Int, changeHandler: (Int, [PeerField: ChangeHandlers.FieldChange]) -> Void) { func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
if scratchpad.isEmpty { if scratchpad.isEmpty {
populateScratchpad() populateScratchpad()
} }
let otherScratchPad = PeerData.createScratchPad(from: other) let otherScratchPad = PeerData.createScratchPad(from: other)
var changes = [PeerField: ChangeHandlers.FieldChange]() var changes = [PeerField: Changes.FieldChange]()
for field in PeerField.allCases { for field in PeerField.allCases {
switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") { switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
case ("", ""): case ("", ""):
@@ -457,14 +469,12 @@ class TunnelViewModel {
changes[field] = .removed changes[field] = .removed
case (let this, let other): case (let this, let other):
if this != other { if this != other {
changes[field] = .modified changes[field] = .modified(newValue: other)
} }
} }
} }
scratchpad = otherScratchPad scratchpad = otherScratchPad
if !changes.isEmpty { return changes
changeHandler(peerIndex, changes)
}
} }
} }
@@ -519,6 +529,17 @@ class TunnelViewModel {
} }
} }
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> { func save() -> SaveResult<TunnelConfiguration> {
let interfaceSaveResult = interfaceData.save() let interfaceSaveResult = interfaceData.save()
let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
@@ -548,21 +569,28 @@ class TunnelViewModel {
} }
} }
func applyConfiguration(other: TunnelConfiguration, changeHandlers: ChangeHandlers) { func asWgQuickConfig() -> String? {
let saveResult = save()
if case .saved(let tunnelConfiguration) = saveResult {
return tunnelConfiguration.asWgQuickConfig()
}
return nil
}
@discardableResult
func applyConfiguration(other: TunnelConfiguration) -> Changes {
// Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering. // Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
// Change handler callbacks are processed in the following order, which is designed to work with both the
// UITableView way (modify - delete - insert) and the NSTableView way (indices are based on past operations):
// - interfaceChanged
// - peerChangedAt
// - peersRemovedAt
// - peersInsertedAt
interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "", changeHandler: changeHandlers.interfaceChanged) let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
for otherPeer in other.peers { for otherPeer in other.peers {
if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) { if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
let peerData = peersData[peersDataIndex] let peerData = peersData[peersDataIndex]
peerData.applyConfiguration(other: otherPeer, peerIndex: peersDataIndex, changeHandler: changeHandlers.peerChangedAt) let changes = peerData.applyConfiguration(other: otherPeer)
if !changes.isEmpty {
peerChanges.append((peerIndex: peersDataIndex, changes: changes))
}
} }
} }
@@ -573,9 +601,6 @@ class TunnelViewModel {
peersData.remove(at: index) peersData.remove(at: index)
} }
} }
if !removedPeerIndices.isEmpty {
changeHandlers.peersRemovedAt(removedPeerIndices)
}
var addedPeerIndices = [Int]() var addedPeerIndices = [Int]()
for otherPeer in other.peers { for otherPeer in other.peers {
@@ -586,61 +611,14 @@ class TunnelViewModel {
peersData.append(peerData) peersData.append(peerData)
} }
} }
if !addedPeerIndices.isEmpty {
changeHandlers.peersInsertedAt(addedPeerIndices)
}
for (index, peer) in peersData.enumerated() { for (index, peer) in peersData.enumerated() {
peer.index = index peer.index = index
peer.numberOfPeers = peersData.count peer.numberOfPeers = peersData.count
peer.updateExcludePrivateIPsFieldState() peer.updateExcludePrivateIPsFieldState()
} }
}
}
extension TunnelViewModel { return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
static func activateOnDemandOptionText(for activateOnDemandOption: ActivateOnDemandOption) -> String {
switch activateOnDemandOption {
case .none:
return tr("tunnelOnDemandOptionOff")
case .useOnDemandOverWiFiOnly:
return tr("tunnelOnDemandOptionWiFiOnly")
#if os(iOS)
case .useOnDemandOverWiFiOrCellular:
return tr("tunnelOnDemandOptionWiFiOrCellular")
case .useOnDemandOverCellularOnly:
return tr("tunnelOnDemandOptionCellularOnly")
#elseif os(macOS)
case .useOnDemandOverWiFiOrEthernet:
return tr("tunnelOnDemandOptionWiFiOrEthernet")
case .useOnDemandOverEthernetOnly:
return tr("tunnelOnDemandOptionEthernetOnly")
#else
#error("Unimplemented")
#endif
}
}
static func activateOnDemandDetailText(for activateOnDemandSetting: ActivateOnDemandSetting?) -> String {
if let activateOnDemandSetting = activateOnDemandSetting {
if activateOnDemandSetting.isActivateOnDemandEnabled {
return TunnelViewModel.activateOnDemandOptionText(for: activateOnDemandSetting.activateOnDemandOption)
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
}
} else {
return TunnelViewModel.activateOnDemandOptionText(for: .none)
}
}
static func defaultActivateOnDemandOption() -> ActivateOnDemandOption {
#if os(iOS)
return .useOnDemandOverWiFiOrCellular
#elseif os(macOS)
return .useOnDemandOverWiFiOrEthernet
#else
#error("Unimplemented")
#endif
} }
} }
+26 -7
View File
@@ -9,9 +9,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
var mainVC: MainViewController? var mainVC: MainViewController?
var isLaunchedForSpecificAction = false
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path) Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
if let launchOptions = launchOptions {
if launchOptions[.url] != nil || launchOptions[.shortcutItem] != nil {
isLaunchedForSpecificAction = true
}
}
let window = UIWindow(frame: UIScreen.main.bounds) let window = UIWindow(frame: UIScreen.main.bounds)
window.backgroundColor = .white window.backgroundColor = .white
@@ -27,16 +34,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
guard let tunnelsManager = mainVC?.tunnelsManager else { return true } mainVC?.importFromDisposableFile(url: url)
TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: mainVC, errorPresenterType: ErrorPresenter.self) {
_ = FileManager.deleteFile(at: url)
}
return true return true
} }
func applicationDidBecomeActive(_ application: UIApplication) { func applicationDidBecomeActive(_ application: UIApplication) {
mainVC?.refreshTunnelConnectionStatuses() mainVC?.refreshTunnelConnectionStatuses()
} }
func applicationWillResignActive(_ application: UIApplication) {
guard let allTunnelNames = mainVC?.allTunnelNames() else { return }
application.shortcutItems = QuickActionItem.createItems(allTunnelNames: allTunnelNames)
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
guard shortcutItem.type == QuickActionItem.type else {
completionHandler(false)
return
}
let tunnelName = shortcutItem.localizedTitle
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: true)
completionHandler(true)
}
} }
extension AppDelegate { extension AppDelegate {
@@ -45,7 +64,7 @@ extension AppDelegate {
} }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true return !self.isLaunchedForSpecificAction
} }
func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? { func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
@@ -58,7 +77,7 @@ extension AppDelegate {
} }
} else { } else {
// Show it when tunnelsManager is available // Show it when tunnelsManager is available
mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false) mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: false)
} }
} }
return nil return nil
@@ -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)
}
}
+4 -3
View File
@@ -64,6 +64,8 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string> <string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
@@ -77,12 +79,11 @@
<key>LSSupportsOpeningDocumentsInPlace</key> <key>LSSupportsOpeningDocumentsInPlace</key>
<false/> <false/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Camera is used for scanning QR codes for importing WireGuard configurations</string> <string>Localized</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>armv7</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
@@ -123,7 +124,7 @@
</dict> </dict>
</array> </array>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Face ID is used for authenticating viewing and exporting of private keys</string> <string>Localized</string>
<key>com.wireguard.ios.app_group_id</key> <key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID_IOS)</string> <string>group.$(APP_ID_IOS)</string>
</dict> </dict>
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class QuickActionItem: UIApplicationShortcutItem {
static let type = "WireGuardTunnelActivateAndShow"
init(tunnelName: String) {
super.init(type: QuickActionItem.type, localizedTitle: tunnelName, localizedSubtitle: nil, icon: nil, userInfo: nil)
}
static func createItems(allTunnelNames: [String]) -> [QuickActionItem] {
let numberOfItems = 10
// Currently, only 4 items shown by iOS, but that can increase in the future.
// iOS will discard additional items we give it.
var tunnelNames = RecentTunnelsTracker.recentlyActivatedTunnelNames(limit: numberOfItems)
let numberOfSlotsRemaining = numberOfItems - tunnelNames.count
if numberOfSlotsRemaining > 0 {
let moreTunnels = allTunnelNames.filter { !tunnelNames.contains($0) }.prefix(numberOfSlotsRemaining)
tunnelNames.append(contentsOf: moreTunnels)
}
return tunnelNames.map { QuickActionItem(tunnelName: $0) }
}
}
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Foundation
class RecentTunnelsTracker {
private static let keyRecentlyActivatedTunnelNames = "recentlyActivatedTunnelNames"
private static let maxNumberOfTunnels = 10
private static var userDefaults: UserDefaults? {
guard let appGroupId = FileManager.appGroupId else {
wg_log(.error, staticMessage: "Cannot obtain app group ID from bundle for tracking recently used tunnels")
return nil
}
guard let userDefaults = UserDefaults(suiteName: appGroupId) else {
wg_log(.error, staticMessage: "Cannot obtain shared user defaults for tracking recently used tunnels")
return nil
}
return userDefaults
}
static func handleTunnelActivated(tunnelName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
recentTunnels.remove(at: existingIndex)
}
recentTunnels.insert(tunnelName, at: 0)
if recentTunnels.count > maxNumberOfTunnels {
recentTunnels.removeLast(recentTunnels.count - maxNumberOfTunnels)
}
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
static func handleTunnelRemoved(tunnelName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
recentTunnels.remove(at: existingIndex)
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func handleTunnelRenamed(oldName: String, newName: String) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if let existingIndex = recentTunnels.firstIndex(of: oldName) {
recentTunnels[existingIndex] = newName
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func cleanupTunnels(except tunnelNamesToKeep: Set<String>) {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
let oldCount = recentTunnels.count
recentTunnels.removeAll { !tunnelNamesToKeep.contains($0) }
if oldCount != recentTunnels.count {
userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
}
}
static func recentlyActivatedTunnelNames(limit: Int) -> [String] {
guard let userDefaults = RecentTunnelsTracker.userDefaults else { return [] }
var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
if limit < recentTunnels.count {
recentTunnels.removeLast(recentTunnels.count - limit)
}
return recentTunnels
}
}
@@ -9,8 +9,8 @@ class ButtonCell: UITableViewCell {
set(value) { button.setTitle(value, for: .normal) } set(value) { button.setTitle(value, for: .normal) }
} }
var hasDestructiveAction: Bool { var hasDestructiveAction: Bool {
get { return button.tintColor == .red } get { return button.tintColor == .systemRed }
set(value) { button.tintColor = value ? .red : buttonStandardTintColor } set(value) { button.tintColor = value ? .systemRed : buttonStandardTintColor }
} }
var onTapped: (() -> Void)? var onTapped: (() -> Void)?
@@ -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 = ""
}
}
@@ -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
}
}
@@ -9,7 +9,11 @@ class KeyValueCell: UITableViewCell {
let keyLabel = UILabel() let keyLabel = UILabel()
keyLabel.font = UIFont.preferredFont(forTextStyle: .body) keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
keyLabel.adjustsFontForContentSizeCategory = true keyLabel.adjustsFontForContentSizeCategory = true
keyLabel.textColor = .black if #available(iOS 13.0, *) {
keyLabel.textColor = .label
} else {
keyLabel.textColor = .black
}
keyLabel.textAlignment = .left keyLabel.textAlignment = .left
return keyLabel return keyLabel
}() }()
@@ -31,7 +35,11 @@ class KeyValueCell: UITableViewCell {
valueTextField.autocapitalizationType = .none valueTextField.autocapitalizationType = .none
valueTextField.autocorrectionType = .no valueTextField.autocorrectionType = .no
valueTextField.spellCheckingType = .no valueTextField.spellCheckingType = .no
valueTextField.textColor = .gray if #available(iOS 13.0, *) {
valueTextField.textColor = .secondaryLabel
} else {
valueTextField.textColor = .gray
}
return valueTextField return valueTextField
}() }()
@@ -56,10 +64,18 @@ class KeyValueCell: UITableViewCell {
var isValueValid = true { var isValueValid = true {
didSet { didSet {
if isValueValid { if #available(iOS 13.0, *) {
keyLabel.textColor = .black if isValueValid {
keyLabel.textColor = .label
} else {
keyLabel.textColor = .systemRed
}
} else { } else {
keyLabel.textColor = .red if isValueValid {
keyLabel.textColor = .black
} else {
keyLabel.textColor = .red
}
} }
} }
} }
@@ -68,7 +84,7 @@ class KeyValueCell: UITableViewCell {
var isStackedVertically = false var isStackedVertically = false
var contentSizeBasedConstraints = [NSLayoutConstraint]() var contentSizeBasedConstraints = [NSLayoutConstraint]()
var onValueChanged: ((String) -> Void)? var onValueChanged: ((String, String) -> Void)?
var onValueBeingEdited: ((String) -> Void)? var onValueBeingEdited: ((String) -> Void)?
var observationToken: AnyObject? var observationToken: AnyObject?
@@ -206,7 +222,7 @@ extension KeyValueCell: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) { func textFieldDidEndEditing(_ textField: UITextField) {
let isModified = textField.text ?? "" != textFieldValueOnBeginEditing let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
guard isModified else { return } guard isModified else { return }
onValueChanged?(textField.text ?? "") onValueChanged?(textFieldValueOnBeginEditing, textField.text ?? "")
} }
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
@@ -16,7 +16,11 @@ class SwitchCell: UITableViewCell {
get { return switchView.isEnabled } get { return switchView.isEnabled }
set(value) { set(value) {
switchView.isEnabled = value switchView.isEnabled = value
textLabel?.textColor = value ? .black : .gray if #available(iOS 13.0, *) {
textLabel?.textColor = value ? .label : .secondaryLabel
} else {
textLabel?.textColor = value ? .black : .gray
}
} }
} }
@@ -0,0 +1,38 @@
// 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 = ""
if #available(iOS 13.0, *) {
setTextColor(.label)
} else {
setTextColor(.black)
}
setTextAlignment(.left)
}
}
@@ -30,7 +30,11 @@ class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
copyableGesture = false copyableGesture = false
valueTextField.textColor = .black if #available(iOS 13.0, *) {
valueTextField.textColor = .label
} else {
valueTextField.textColor = .black
}
valueTextField.isEnabled = true valueTextField.isEnabled = true
valueLabelScrollView.isScrollEnabled = false valueLabelScrollView.isScrollEnabled = false
valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
@@ -29,9 +29,15 @@ class TunnelListCell: UITableViewCell {
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) if #available(iOS 13.0, *) {
busyIndicator.hidesWhenStopped = true let busyIndicator = UIActivityIndicatorView(style: .medium)
return busyIndicator busyIndicator.hidesWhenStopped = true
return busyIndicator
} else {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}
}() }()
let statusSwitch = UISwitch() let statusSwitch = UISwitch()
@@ -98,6 +104,11 @@ class TunnelListCell: UITableViewCell {
fatalError("init(coder:) has not been implemented") 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() { private func reset() {
statusSwitch.isOn = false statusSwitch.isOn = false
statusSwitch.isUserInteractionEnabled = false statusSwitch.isUserInteractionEnabled = false
@@ -0,0 +1,155 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import UIKit
class LogViewController: UIViewController {
let textView: UITextView = {
let textView = UITextView()
textView.isEditable = false
textView.isSelectable = true
textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
textView.adjustsFontForContentSizeCategory = true
return textView
}()
let busyIndicator: UIActivityIndicatorView = {
if #available(iOS 13.0, *) {
let busyIndicator = UIActivityIndicatorView(style: .medium)
busyIndicator.hidesWhenStopped = true
return busyIndicator
} else {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}
}()
let paragraphStyle: NSParagraphStyle = {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.setParagraphStyle(NSParagraphStyle.default)
paragraphStyle.lineHeightMultiple = 1.2
return paragraphStyle
}()
var isNextLineHighlighted = false
var logViewHelper: LogViewHelper?
var isFetchingLogEntries = false
private var updateLogEntriesTimer: Timer?
override func loadView() {
view = UIView()
view.backgroundColor = .white
view.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
textView.topAnchor.constraint(equalTo: view.topAnchor),
textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
view.addSubview(busyIndicator)
busyIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
busyIndicator.startAnimating()
logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
startUpdatingLogEntries()
}
override func viewDidLoad() {
title = tr("logViewTitle")
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped(sender:)))
}
func updateLogEntries() {
guard !isFetchingLogEntries else { return }
isFetchingLogEntries = true
logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in
guard let self = self else { return }
defer {
self.isFetchingLogEntries = false
}
if self.busyIndicator.isAnimating {
self.busyIndicator.stopAnimating()
}
guard !fetchedLogEntries.isEmpty else { return }
let isScrolledToEnd = self.textView.contentSize.height - self.textView.bounds.height - self.textView.contentOffset.y < 1
let richText = NSMutableAttributedString()
let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
let lightGrayColor = UIColor(white: 0.88, alpha: 1.0)
for logEntry in fetchedLogEntries {
let bgColor = self.isNextLineHighlighted ? lightGrayColor : UIColor.white
let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .paragraphStyle: self.paragraphStyle])
richText.append(timestampText)
richText.append(messageText)
self.isNextLineHighlighted.toggle()
}
self.textView.textStorage.append(richText)
if isScrolledToEnd {
let endOfCurrentText = NSRange(location: (self.textView.text as NSString).length, length: 0)
self.textView.scrollRangeToVisible(endOfCurrentText)
}
}
}
func startUpdatingLogEntries() {
updateLogEntries()
updateLogEntriesTimer?.invalidate()
let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
self?.updateLogEntries()
}
updateLogEntriesTimer = timer
RunLoop.main.add(timer, forMode: .common)
}
@objc func saveTapped(sender: AnyObject) {
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)
if let sender = sender as? UIBarButtonItem {
activityVC.popoverPresentationController?.barButtonItem = sender
}
activityVC.completionWithItemsHandler = { _, _, _, _ in
// Remove the exported log file after the activity has completed
_ = FileManager.deleteFile(at: destinationURL)
}
self.present(activityVC, animated: true)
}
}
}
}
@@ -42,22 +42,25 @@ class MainViewController: UISplitViewController {
TunnelsManager.create { [weak self] result in TunnelsManager.create { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
if let error = result.error { switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
return case .success(let tunnelsManager):
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = self
self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
} }
let tunnelsManager: TunnelsManager = result.value!
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)
tunnelsManager.activationDelegate = self
self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
} }
} }
func allTunnelNames() -> [String]? {
guard let tunnelsManager = self.tunnelsManager else { return nil }
return tunnelsManager.mapTunnels { $0.name }
}
} }
extension MainViewController: TunnelsManagerActivationDelegate { extension MainViewController: TunnelsManagerActivationDelegate {
@@ -85,19 +88,17 @@ extension MainViewController {
} }
} }
func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool) { func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool, shouldToggleStatus: Bool) {
let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
guard let self = self else { return }
guard let tunnelsListVC = self.tunnelsListVC else { return }
if let tunnel = tunnelsManager.tunnel(named: tunnelName) { if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) tunnelsListVC.showTunnelDetail(for: tunnel, animated: false)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC) if shouldToggleStatus {
tunnelDetailNC.restorationIdentifier = "DetailNC" if tunnel.status == .inactive {
if let self = self { tunnelsManager.startActivation(of: tunnel)
if animated { } else if tunnel.status == .active {
self.showDetailViewController(tunnelDetailNC, sender: self) tunnelsManager.startDeactivation(of: tunnel)
} else {
UIView.performWithoutAnimation {
self.showDetailViewController(tunnelDetailNC, sender: self)
}
} }
} }
} }
@@ -108,6 +109,19 @@ extension MainViewController {
onTunnelsManagerReady = showTunnelDetailBlock onTunnelsManagerReady = showTunnelDetailBlock
} }
} }
func importFromDisposableFile(url: URL) {
let importFromFileBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) {
_ = FileManager.deleteFile(at: url)
}
}
if let tunnelsManager = tunnelsManager {
importFromFileBlock(tunnelsManager)
} else {
onTunnelsManagerReady = importFromFileBlock
}
}
} }
extension MainViewController: UISplitViewControllerDelegate { extension MainViewController: UISplitViewControllerDelegate {
@@ -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
}
}
@@ -0,0 +1,303 @@
// 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")
if #available(iOS 13.0, *) {
cell.setTextColor(.secondaryLabel)
} else {
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]
guard previousOption != selectedOption else {
tableView.deselectRow(at: indexPath, animated: true)
return
}
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
}
@@ -10,22 +10,24 @@ class SettingsTableViewController: UITableViewController {
case iosAppVersion case iosAppVersion
case goBackendVersion case goBackendVersion
case exportZipArchive case exportZipArchive
case exportLogFile case viewLog
case donateLink
var localizedUIString: String { var localizedUIString: String {
switch self { switch self {
case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS") case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend") case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
case .exportZipArchive: return tr("settingsExportZipButtonTitle") case .exportZipArchive: return tr("settingsExportZipButtonTitle")
case .exportLogFile: return tr("settingsExportLogFileButtonTitle") case .viewLog: return tr("settingsViewLogButtonTitle")
case .donateLink: return tr("donateLink")
} }
} }
} }
let settingsFieldsBySection: [[SettingsFields]] = [ let settingsFieldsBySection: [[SettingsFields]] = [
[.iosAppVersion, .goBackendVersion], [.iosAppVersion, .goBackendVersion, .donateLink],
[.exportZipArchive], [.exportZipArchive],
[.exportLogFile] [.viewLog]
] ]
let tunnelsManager: TunnelsManager? let tunnelsManager: TunnelsManager?
@@ -108,46 +110,10 @@ class SettingsTableViewController: UITableViewController {
} }
} }
func exportLogForLastActivatedTunnel(sourceView: UIView) { func presentLogView() {
guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } let logVC = LogViewController()
navigationController?.pushViewController(logVC, animated: true)
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
let timeStampString = dateFormatter.string(from: Date())
let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt")
DispatchQueue.global(qos: .userInitiated).async {
if FileManager.default.fileExists(atPath: destinationURL.path) {
let isDeleted = FileManager.deleteFile(at: destinationURL)
if !isDeleted {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
return
}
}
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
return
}
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
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)
}
}
} }
} }
@@ -196,14 +162,23 @@ extension SettingsTableViewController {
self?.exportConfigurationsAsZipFile(sourceView: cell.button) self?.exportConfigurationsAsZipFile(sourceView: cell.button)
} }
return cell return cell
} else { } else if field == .viewLog {
assert(field == .exportLogFile)
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath) let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.localizedUIString cell.buttonText = field.localizedUIString
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
self?.exportLogForLastActivatedTunnel(sourceView: cell.button) self?.presentLogView()
}
return cell
} else if field == .donateLink {
let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
cell.buttonText = field.localizedUIString
cell.onTapped = {
if let url = URL(string: "https://www.wireguard.com/donations/"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
}
} }
return cell return cell
} }
fatalError()
} }
} }
@@ -24,21 +24,28 @@ class TunnelDetailTableViewController: UITableViewController {
.rxBytes, .txBytes, .lastHandshakeTime .rxBytes, .txBytes, .lastHandshakeTime
] ]
static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
.onDemand, .ssid
]
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer let tunnel: TunnelContainer
var tunnelViewModel: TunnelViewModel var tunnelViewModel: TunnelViewModel
var onDemandViewModel: ActivateOnDemandViewModel
private var sections = [Section]() private var sections = [Section]()
private var interfaceFieldIsVisible = [Bool]() private var interfaceFieldIsVisible = [Bool]()
private var peerFieldIsVisible = [[Bool]]() private var peerFieldIsVisible = [[Bool]]()
private var statusObservationToken: AnyObject? private var statusObservationToken: AnyObject?
private var onDemandObservationToken: AnyObject?
private var reloadRuntimeConfigurationTimer: Timer? private var reloadRuntimeConfigurationTimer: Timer?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
loadVisibleFields() loadVisibleFields()
@@ -51,6 +58,11 @@ class TunnelDetailTableViewController: UITableViewController {
self.stopUpdatingRuntimeConfiguration() 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) { required init?(coder aDecoder: NSCoder) {
@@ -64,10 +76,10 @@ class TunnelDetailTableViewController: UITableViewController {
tableView.estimatedRowHeight = 44 tableView.estimatedRowHeight = 44
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.allowsSelection = false
tableView.register(SwitchCell.self) tableView.register(SwitchCell.self)
tableView.register(KeyValueCell.self) tableView.register(KeyValueCell.self)
tableView.register(ButtonCell.self) tableView.register(ButtonCell.self)
tableView.register(ChevronCell.self)
restorationIdentifier = "TunnelDetailVC:\(tunnel.name)" restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
} }
@@ -108,26 +120,11 @@ class TunnelDetailTableViewController: UITableViewController {
let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel) let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
editVC.delegate = self editVC.delegate = self
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet editNC.modalPresentationStyle = .fullScreen
self.present(editNC, animated: true) self.present(editNC, animated: true)
} }
} }
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
func startUpdatingRuntimeConfiguration() { func startUpdatingRuntimeConfiguration() {
reloadRuntimeConfiguration() reloadRuntimeConfiguration()
reloadRuntimeConfigurationTimer?.invalidate() reloadRuntimeConfigurationTimer?.invalidate()
@@ -158,16 +155,21 @@ class TunnelDetailTableViewController: UITableViewController {
var interfaceFieldIsVisible = self.interfaceFieldIsVisible var interfaceFieldIsVisible = self.interfaceFieldIsVisible
var peerFieldIsVisible = self.peerFieldIsVisible var peerFieldIsVisible = self.peerFieldIsVisible
func sectionChanged<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], tableView: UITableView, section: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) { 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 fieldIsVisible = fieldIsVisibleInput
var modifiedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated() where changes[field] == .modified {
let row = fieldIsVisible[0 ..< index].filter { $0 }.count
modifiedIndexPaths.append(IndexPath(row: row, section: section))
}
if !modifiedIndexPaths.isEmpty {
tableView.reloadRows(at: modifiedIndexPaths, with: .none)
}
var removedIndexPaths = [IndexPath]() var removedIndexPaths = [IndexPath]()
for (index, field) in fields.enumerated().reversed() where changes[field] == .removed { for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
@@ -190,30 +192,44 @@ class TunnelDetailTableViewController: UITableViewController {
} }
} }
let changeHandlers = TunnelViewModel.ChangeHandlers( let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
interfaceChanged: { changes in
sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
tableView: tableView, section: interfaceSectionIndex, changes: changes)
},
peerChangedAt: { peerIndex, changes in
sectionChanged(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex],
tableView: tableView, section: firstPeerSectionIndex + peerIndex, changes: changes)
},
peersRemovedAt: { peerIndices in
let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
tableView.deleteSections(IndexSet(sectionIndices), with: .automatic)
},
peersInsertedAt: { peerIndices in
let sectionIndices = peerIndices.map { firstPeerSectionIndex + $0 }
tableView.insertSections(IndexSet(sectionIndices), with: .automatic)
}
)
tableView.beginUpdates() if !changes.interfaceChanges.isEmpty {
self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers) handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
self.loadSections() section: interfaceSectionIndex, changes: changes.interfaceChanges)
self.loadVisibleFields() }
tableView.endUpdates() 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() { private func reloadRuntimeConfiguration() {
@@ -223,11 +239,27 @@ class TunnelDetailTableViewController: UITableViewController {
self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration) 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 { extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) { func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
loadSections() loadSections()
loadVisibleFields() loadVisibleFields()
title = tunnel.name title = tunnel.name
@@ -253,7 +285,7 @@ extension TunnelDetailTableViewController {
case .peer(let peerIndex, _): case .peer(let peerIndex, _):
return peerFieldIsVisible[peerIndex].filter { $0 }.count return peerFieldIsVisible[peerIndex].filter { $0 }.count
case .onDemand: case .onDemand:
return 1 return onDemandViewModel.isWiFiInterfaceEnabled ? 2 : 1
case .delete: case .delete:
return 1 return 1
} }
@@ -351,6 +383,8 @@ extension TunnelDetailTableViewController {
cell.key = field.localizedUIString cell.key = field.localizedUIString
if field == .persistentKeepAlive { if field == .persistentKeepAlive {
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field]) cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
} else if field == .preSharedKey {
cell.value = tr("tunnelPeerPresharedKeyEnabled")
} else { } else {
cell.value = peerData[field] cell.value = peerData[field]
} }
@@ -358,13 +392,26 @@ extension TunnelDetailTableViewController {
} }
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath) let field = TunnelDetailTableViewController.onDemandFields[indexPath.row]
cell.key = tr("tunnelOnDemandKey") if field == .onDemand {
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting) let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
cell.observationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak cell] tunnel, _ in cell.key = field.localizedUIString
cell?.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting) 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
}
} }
return cell
} }
private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
@@ -373,7 +420,9 @@ extension TunnelDetailTableViewController {
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self] in cell.onTapped = { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"), buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"), from: cell) { [weak self] in ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"),
buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"),
from: cell, presentingVC: self) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.tunnelsManager.remove(tunnel: self.tunnel) { error in self.tunnelsManager.remove(tunnel: self.tunnel) { error in
if error != nil { if error != nil {
@@ -387,3 +436,22 @@ extension TunnelDetailTableViewController {
} }
} }
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)
}
}
@@ -43,16 +43,16 @@ class TunnelEditTableViewController: UITableViewController {
.deletePeer .deletePeer
] ]
let activateOnDemandOptions: [ActivateOnDemandOption] = [ let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
.useOnDemandOverWiFiOrCellular, .nonWiFiInterface,
.useOnDemandOverWiFiOnly, .wiFiInterface,
.useOnDemandOverCellularOnly .ssid
] ]
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer? let tunnel: TunnelContainer?
let tunnelViewModel: TunnelViewModel let tunnelViewModel: TunnelViewModel
var activateOnDemandSetting: ActivateOnDemandSetting var onDemandViewModel: ActivateOnDemandViewModel
private var sections = [Section]() private var sections = [Section]()
// Use this initializer to edit an existing tunnel. // Use this initializer to edit an existing tunnel.
@@ -60,7 +60,7 @@ class TunnelEditTableViewController: UITableViewController {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
activateOnDemandSetting = tunnel.activateOnDemandSetting onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
} }
@@ -70,7 +70,7 @@ class TunnelEditTableViewController: UITableViewController {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnel = nil tunnel = nil
tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil) tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
activateOnDemandSetting = ActivateOnDemandSetting.defaultSetting onDemandViewModel = ActivateOnDemandViewModel()
super.init(style: .grouped) super.init(style: .grouped)
loadSections() loadSections()
} }
@@ -92,7 +92,7 @@ class TunnelEditTableViewController: UITableViewController {
tableView.register(TunnelEditEditableKeyValueCell.self) tableView.register(TunnelEditEditableKeyValueCell.self)
tableView.register(ButtonCell.self) tableView.register(ButtonCell.self)
tableView.register(SwitchCell.self) tableView.register(SwitchCell.self)
tableView.register(CheckmarkCell.self) tableView.register(ChevronCell.self)
} }
private func loadSections() { private func loadSections() {
@@ -113,9 +113,10 @@ class TunnelEditTableViewController: UITableViewController {
ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self) ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
tableView.reloadData() // Highlight erroring fields tableView.reloadData() // Highlight erroring fields
case .saved(let tunnelConfiguration): case .saved(let tunnelConfiguration):
let onDemandOption = onDemandViewModel.toOnDemandOption()
if let tunnel = tunnel { if let tunnel = tunnel {
// We're modifying an existing tunnel // We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] error in tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { } else {
@@ -125,11 +126,11 @@ class TunnelEditTableViewController: UITableViewController {
} }
} else { } else {
// We're adding a new tunnel // We're adding a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: activateOnDemandSetting) { [weak self] result in tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
if let error = result.error { switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
} else { case .success(let tunnel):
let tunnel: TunnelContainer = result.value!
self?.dismiss(animated: true, completion: nil) self?.dismiss(animated: true, completion: nil)
self?.delegate?.tunnelSaved(tunnel: tunnel) self?.delegate?.tunnelSaved(tunnel: tunnel)
} }
@@ -161,10 +162,10 @@ extension TunnelEditTableViewController {
case .addPeer: case .addPeer:
return 1 return 1
case .onDemand: case .onDemand:
if activateOnDemandSetting.isActivateOnDemandEnabled { if onDemandViewModel.isWiFiInterfaceEnabled {
return 4 return 3
} else { } else {
return 1 return 2
} }
} }
} }
@@ -250,6 +251,8 @@ extension TunnelEditTableViewController {
cell.keyboardType = .numberPad cell.keyboardType = .numberPad
case .publicKey, .generateKeyPair: case .publicKey, .generateKeyPair:
cell.keyboardType = .default cell.keyboardType = .default
case .status, .toggleStatus:
fatalError("Unexpected interface field")
} }
cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field)) cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
@@ -259,8 +262,18 @@ extension TunnelEditTableViewController {
cell.onValueBeingEdited = { [weak self] value in cell.onValueBeingEdited = { [weak self] value in
self?.tunnelViewModel.interfaceData[field] = value 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 { } else {
cell.onValueChanged = { [weak self] value in cell.onValueChanged = { [weak self] _, value in
self?.tunnelViewModel.interfaceData[field] = value self?.tunnelViewModel.interfaceData[field] = value
} }
} }
@@ -298,7 +311,9 @@ extension TunnelEditTableViewController {
cell.hasDestructiveAction = true cell.hasDestructiveAction = true
cell.onTapped = { [weak self, weak peerData] in cell.onTapped = { [weak self, weak peerData] in
guard let self = self, let peerData = peerData else { return } guard let self = self, let peerData = peerData else { return }
self.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"), buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"), from: cell) { [weak self] in ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"),
buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"),
from: cell, presentingVC: self) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
let removedSectionIndices = self.deletePeer(peer: peerData) let removedSectionIndices = self.deletePeer(peer: peerData)
let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl) let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
@@ -380,7 +395,7 @@ extension TunnelEditTableViewController {
tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none) tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
} }
} else { } else {
cell.onValueChanged = { [weak peerData] value in cell.onValueChanged = { [weak peerData] _, value in
peerData?[field] = value peerData?[field] = value
} }
} }
@@ -409,36 +424,29 @@ extension TunnelEditTableViewController {
} }
private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 { let field = onDemandFields[indexPath.row]
if indexPath.row < 2 {
let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath) let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
cell.message = tr("tunnelOnDemandKey") cell.message = field.localizedUIString
cell.isOn = activateOnDemandSetting.isActivateOnDemandEnabled cell.isOn = onDemandViewModel.isEnabled(field: field)
cell.onSwitchToggled = { [weak self] isOn in cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return } guard let self = self else { return }
guard isOn != self.activateOnDemandSetting.isActivateOnDemandEnabled else { return } self.onDemandViewModel.setEnabled(field: field, isEnabled: isOn)
self.activateOnDemandSetting.isActivateOnDemandEnabled = isOn
self.loadSections()
let section = self.sections.firstIndex { $0 == .onDemand }! let section = self.sections.firstIndex { $0 == .onDemand }!
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: section) } let indexPath = IndexPath(row: 2, section: section)
if isOn { if field == .wiFiInterface {
if self.activateOnDemandSetting.activateOnDemandOption == .none { if isOn {
self.activateOnDemandSetting.activateOnDemandOption = TunnelViewModel.defaultActivateOnDemandOption() tableView.insertRows(at: [indexPath], with: .fade)
} else {
tableView.deleteRows(at: [indexPath], with: .fade)
} }
self.tableView.insertRows(at: indexPaths, with: .fade)
} else {
self.tableView.deleteRows(at: indexPaths, with: .fade)
} }
} }
return cell return cell
} else { } else {
let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath) let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
let rowOption = activateOnDemandOptions[indexPath.row - 1] cell.message = field.localizedUIString
let selectedOption = activateOnDemandSetting.activateOnDemandOption cell.detailMessage = onDemandViewModel.localizedSSIDDescription
assert(selectedOption != .none)
cell.message = TunnelViewModel.activateOnDemandOptionText(for: rowOption)
cell.isChecked = selectedOption == rowOption
return cell return cell
} }
} }
@@ -455,26 +463,11 @@ extension TunnelEditTableViewController {
loadSections() loadSections()
return IndexSet(integer: interfaceFieldsBySection.count + peer.index) return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
} }
func showConfirmationAlert(message: String, buttonTitle: String, from sourceView: UIView, onConfirmed: @escaping (() -> Void)) {
let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
onConfirmed()
}
let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
alert.addAction(destroyAction)
alert.addAction(cancelAction)
alert.popoverPresentationController?.sourceView = sourceView
alert.popoverPresentationController?.sourceRect = sourceView.bounds
present(alert, animated: true, completion: nil)
}
} }
extension TunnelEditTableViewController { extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if case .onDemand = sections[indexPath.section], indexPath.row > 0 { if case .onDemand = sections[indexPath.section], indexPath.row == 2 {
return indexPath return indexPath
} else { } else {
return nil return nil
@@ -484,16 +477,27 @@ extension TunnelEditTableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch sections[indexPath.section] { switch sections[indexPath.section] {
case .onDemand: case .onDemand:
let option = activateOnDemandOptions[indexPath.row - 1] assert(indexPath.row == 2)
assert(option != .none) tableView.deselectRow(at: indexPath, animated: true)
activateOnDemandSetting.activateOnDemandOption = option let ssidOptionVC = SSIDOptionEditTableViewController(option: onDemandViewModel.ssidOption, ssids: onDemandViewModel.selectedSSIDs)
ssidOptionVC.delegate = self
let indexPaths = (1 ..< 4).map { IndexPath(row: $0, section: indexPath.section) } navigationController?.pushViewController(ssidOptionVC, animated: true)
UIView.performWithoutAnimation {
tableView.reloadRows(at: indexPaths, with: .none)
}
default: default:
assertionFailure() 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)
}
}
}
}
@@ -9,6 +9,12 @@ class TunnelsListTableViewController: UIViewController {
var tunnelsManager: TunnelsManager? var tunnelsManager: TunnelsManager?
enum TableState: Equatable {
case normal
case rowSwiped
case multiSelect(selectionCount: Int)
}
let tableView: UITableView = { let tableView: UITableView = {
let tableView = UITableView(frame: CGRect.zero, style: .plain) let tableView = UITableView(frame: CGRect.zero, style: .plain)
tableView.estimatedRowHeight = 60 tableView.estimatedRowHeight = 60
@@ -26,12 +32,23 @@ class TunnelsListTableViewController: UIViewController {
}() }()
let busyIndicator: UIActivityIndicatorView = { let busyIndicator: UIActivityIndicatorView = {
let busyIndicator = UIActivityIndicatorView(style: .gray) if #available(iOS 13.0, *) {
busyIndicator.hidesWhenStopped = true let busyIndicator = UIActivityIndicatorView(style: .medium)
return busyIndicator busyIndicator.hidesWhenStopped = true
return busyIndicator
} else {
let busyIndicator = UIActivityIndicatorView(style: .gray)
busyIndicator.hidesWhenStopped = true
return busyIndicator
}
}() }()
var detailDisplayedTunnel: TunnelContainer? var detailDisplayedTunnel: TunnelContainer?
var tableState: TableState = .normal {
didSet {
handleTableStateChange()
}
}
override func loadView() { override func loadView() {
view = UIView() view = UIView()
@@ -74,13 +91,39 @@ class TunnelsListTableViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
title = tr("tunnelsListTitle") tableState = .normal
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
restorationIdentifier = "TunnelsListVC" restorationIdentifier = "TunnelsListVC"
} }
func handleTableStateChange() {
switch tableState {
case .normal:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
case .rowSwiped:
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectButtonTitle"), style: .plain, target: self, action: #selector(selectButtonTapped))
case .multiSelect(let selectionCount):
if selectionCount > 0 {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListDeleteButtonTitle"), style: .plain, target: self, action: #selector(deleteButtonTapped(sender:)))
} else {
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectAllButtonTitle"), style: .plain, target: self, action: #selector(selectAllButtonTapped))
}
}
if case .multiSelect(let selectionCount) = tableState, selectionCount > 0 {
navigationItem.title = tr(format: "tunnelsListSelectedTitle (%d)", selectionCount)
} else {
navigationItem.title = tr("tunnelsListTitle")
}
if case .multiSelect = tableState {
tableView.allowsMultipleSelectionDuringEditing = true
} else {
tableView.allowsMultipleSelectionDuringEditing = false
}
}
func setTunnelsManager(tunnelsManager: TunnelsManager) { func setTunnelsManager(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
tunnelsManager.tunnelsListDelegate = self tunnelsManager.tunnelsListDelegate = self
@@ -141,7 +184,7 @@ class TunnelsListTableViewController: UIViewController {
func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) { func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager) let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
let editNC = UINavigationController(rootViewController: editVC) let editNC = UINavigationController(rootViewController: editVC)
editNC.modalPresentationStyle = .formSheet editNC.modalPresentationStyle = .fullScreen
present(editNC, animated: true) present(editNC, animated: true)
} }
@@ -159,14 +202,85 @@ class TunnelsListTableViewController: UIViewController {
scanQRCodeNC.modalPresentationStyle = .fullScreen scanQRCodeNC.modalPresentationStyle = .fullScreen
present(scanQRCodeNC, animated: true) present(scanQRCodeNC, animated: true)
} }
@objc func selectButtonTapped() {
let shouldCancelSwipe = tableState == .rowSwiped
tableState = .multiSelect(selectionCount: 0)
if shouldCancelSwipe {
tableView.setEditing(false, animated: false)
}
tableView.setEditing(true, animated: true)
}
@objc func doneButtonTapped() {
tableState = .normal
tableView.setEditing(false, animated: true)
}
@objc func selectAllButtonTapped() {
guard tableView.isEditing else { return }
guard let tunnelsManager = tunnelsManager else { return }
for index in 0 ..< tunnelsManager.numberOfTunnels() {
tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)
}
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
}
@objc func cancelButtonTapped() {
tableState = .normal
tableView.setEditing(false, animated: true)
}
@objc func deleteButtonTapped(sender: AnyObject?) {
guard let sender = sender as? UIBarButtonItem else { return }
guard let tunnelsManager = tunnelsManager else { return }
let selectedTunnelIndices = tableView.indexPathsForSelectedRows?.map { $0.row } ?? []
let selectedTunnels = selectedTunnelIndices.compactMap { tunnelIndex in
tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() ? tunnelsManager.tunnel(at: tunnelIndex) : nil
}
guard !selectedTunnels.isEmpty else { return }
let message = selectedTunnels.count == 1 ?
tr(format: "deleteTunnelConfirmationAlertButtonMessage (%d)", selectedTunnels.count) :
tr(format: "deleteTunnelsConfirmationAlertButtonMessage (%d)", selectedTunnels.count)
let title = tr("deleteTunnelsConfirmationAlertButtonTitle")
ConfirmationAlertPresenter.showConfirmationAlert(message: message, buttonTitle: title,
from: sender, presentingVC: self) { [weak self] in
self?.tunnelsManager?.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
guard let self = self else { return }
if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self)
return
}
self.tableState = .normal
self.tableView.setEditing(false, animated: true)
}
}
}
func showTunnelDetail(for tunnel: TunnelContainer, animated: Bool) {
guard let tunnelsManager = tunnelsManager else { return }
guard let splitViewController = splitViewController else { return }
guard let navController = navigationController else { return }
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
tunnel: tunnel)
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC"
if splitViewController.isCollapsed && navController.viewControllers.count > 1 {
navController.setViewControllers([self, tunnelDetailNC], animated: animated)
} else {
splitViewController.showDetailViewController(tunnelDetailNC, sender: self, animated: animated)
}
detailDisplayedTunnel = tunnel
self.presentedViewController?.dismiss(animated: false, completion: nil)
}
} }
extension TunnelsListTableViewController: UIDocumentPickerDelegate { extension TunnelsListTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
urls.forEach { url in TunnelImporter.importFromFile(urls: urls, into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self)
TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self)
}
} }
} }
@@ -174,9 +288,10 @@ extension TunnelsListTableViewController: QRScanViewControllerDelegate {
func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
completionHandler: (() -> Void)?) { completionHandler: (() -> Void)?) {
tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
if let error = result.error { switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler) ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
} else { case .success:
completionHandler?() completionHandler?()
} }
} }
@@ -212,14 +327,20 @@ extension TunnelsListTableViewController: UITableViewDataSource {
extension TunnelsListTableViewController: UITableViewDelegate { extension TunnelsListTableViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard !tableView.isEditing else {
tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
return
}
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
let tunnel = tunnelsManager.tunnel(at: indexPath.row) let tunnel = tunnelsManager.tunnel(at: indexPath.row)
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, showTunnelDetail(for: tunnel, animated: true)
tunnel: tunnel) }
let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
tunnelDetailNC.restorationIdentifier = "DetailNC" func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
showDetailViewController(tunnelDetailNC, sender: self) // Shall get propagated up to the split-vc guard !tableView.isEditing else {
detailDisplayedTunnel = tunnel tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
return
}
} }
func tableView(_ tableView: UITableView, func tableView(_ tableView: UITableView,
@@ -238,6 +359,18 @@ extension TunnelsListTableViewController: UITableViewDelegate {
} }
return UISwipeActionsConfiguration(actions: [deleteAction]) return UISwipeActionsConfiguration(actions: [deleteAction])
} }
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
if tableState == .normal {
tableState = .rowSwiped
}
}
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
if tableState == .rowSwiped {
tableState = .normal
}
}
} }
extension TunnelsListTableViewController: TunnelsManagerListDelegate { extension TunnelsListTableViewController: TunnelsManagerListDelegate {
@@ -273,3 +406,15 @@ extension TunnelsListTableViewController: TunnelsManagerListDelegate {
} }
} }
} }
extension UISplitViewController {
func showDetailViewController(_ vc: UIViewController, sender: Any?, animated: Bool) {
if animated {
showDetailViewController(vc, sender: sender)
} else {
UIView.performWithoutAnimation {
showDetailViewController(vc, sender: sender)
}
}
}
}
@@ -6,6 +6,8 @@
<array> <array>
<string>packet-tunnel-provider</string> <string>packet-tunnel-provider</string>
</array> </array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.$(APP_ID_IOS)</string> <string>group.$(APP_ID_IOS)</string>
+199 -29
View File
@@ -2,6 +2,7 @@
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved. // Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa import Cocoa
import ServiceManagement
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
@@ -12,33 +13,121 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var manageTunnelsRootVC: ManageTunnelsRootViewController? var manageTunnelsRootVC: ManageTunnelsRootViewController?
var manageTunnelsWindowObject: NSWindow? var manageTunnelsWindowObject: NSWindow?
var isTerminationAlertShown = false var onAppDeactivation: (() -> Void)?
func applicationWillFinishLaunching(_ notification: Notification) {
// To workaround a possible AppKit bug that causes the main menu to become unresponsive sometimes
// (especially when launched through Xcode) if we call setActivationPolicy(.regular) in
// in applicationDidFinishLaunching, we set it to .prohibited here.
// Setting it to .regular would fix that problem too, but at this point, we don't know
// whether the app was launched at login or not, so we're not sure whether we should
// show the app icon in the dock or not.
NSApp.setActivationPolicy(.prohibited)
}
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
Logger.configureGlobal(withFilePath: FileManager.appLogFileURL?.path) Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
registerLoginItem(shouldLaunchAtLogin: true)
var isLaunchedAtLogin = false
if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
isLaunchedAtLogin = LaunchedAtLoginDetector.isLaunchedAtLogin(openAppleEvent: appleEvent)
}
NSApp.mainMenu = MainMenu()
setDockIconAndMainMenuVisibility(isVisible: !isLaunchedAtLogin)
TunnelsManager.create { [weak self] result in TunnelsManager.create { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
if let error = result.error {
switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: nil) ErrorPresenter.showErrorAlert(error: error, from: nil)
return case .success(let tunnelsManager):
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
statusMenu.windowDelegate = self
let statusItemController = StatusItemController()
statusItemController.statusItem.menu = statusMenu
let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
tunnelsTracker.statusMenu = statusMenu
tunnelsTracker.statusItemController = statusItemController
self.tunnelsManager = tunnelsManager
self.tunnelsTracker = tunnelsTracker
self.statusItemController = statusItemController
if !isLaunchedAtLogin {
self.showManageTunnelsWindow(completion: nil)
}
} }
}
}
let tunnelsManager: TunnelsManager = result.value! func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
if LaunchedAtLoginDetector.isReopenedByLoginItemHelper(reopenAppleEvent: appleEvent) {
return false
}
}
if hasVisibleWindows {
return true
}
showManageTunnelsWindow(completion: nil)
return false
}
let statusMenu = StatusMenu(tunnelsManager: tunnelsManager) @objc func confirmAndQuit() {
statusMenu.windowDelegate = self let alert = NSAlert()
alert.messageText = tr("macConfirmAndQuitAlertMessage")
if let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating {
alert.informativeText = tr(format: "macConfirmAndQuitInfoWithActiveTunnel (%@)", currentTunnel.name)
} else {
alert.informativeText = tr("macConfirmAndQuitAlertInfo")
}
alert.addButton(withTitle: tr("macConfirmAndQuitAlertCloseWindow"))
alert.addButton(withTitle: tr("macConfirmAndQuitAlertQuitWireGuard"))
let statusItemController = StatusItemController() NSApp.activate(ignoringOtherApps: true)
statusItemController.statusItem.menu = statusMenu if let manageWindow = manageTunnelsWindowObject {
manageWindow.orderFront(self)
alert.beginSheetModal(for: manageWindow) { response in
switch response {
case .alertFirstButtonReturn:
manageWindow.close()
case .alertSecondButtonReturn:
NSApp.terminate(nil)
default:
break
}
}
}
}
let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager) @objc func quit() {
tunnelsTracker.statusMenu = statusMenu if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
tunnelsTracker.statusItemController = statusItemController NSApp.activate(ignoringOtherApps: true)
manageWindow.orderFront(self)
self.tunnelsManager = tunnelsManager return
self.tunnelsTracker = tunnelsTracker }
self.statusItemController = statusItemController registerLoginItem(shouldLaunchAtLogin: false)
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
NSApp.terminate(nil)
return
}
let alert = NSAlert()
alert.messageText = tr("macAppExitingWithActiveTunnelMessage")
alert.informativeText = tr("macAppExitingWithActiveTunnelInfo")
NSApp.activate(ignoringOtherApps: true)
if let manageWindow = manageTunnelsWindowObject {
manageWindow.orderFront(self)
alert.beginSheetModal(for: manageWindow) { _ in
NSApp.terminate(nil)
}
} else {
alert.runModal()
NSApp.terminate(nil)
} }
} }
@@ -46,29 +135,100 @@ class AppDelegate: NSObject, NSApplicationDelegate {
guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else { guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
return .terminateNow return .terminateNow
} }
if isTerminationAlertShown { guard let appleEvent = NSAppleEventManager.shared().currentAppleEvent else {
return .terminateNow
}
guard MacAppStoreUpdateDetector.isUpdatingFromMacAppStore(quitAppleEvent: appleEvent) else {
return .terminateNow return .terminateNow
} }
let alert = NSAlert() let alert = NSAlert()
alert.messageText = tr("macAppExitingWithActiveTunnelMessage") alert.messageText = tr("macAppStoreUpdatingAlertMessage")
alert.informativeText = tr("macAppExitingWithActiveTunnelInfo") if currentTunnel.isActivateOnDemandEnabled {
if let window = manageTunnelsWindowObject { alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithOnDemand (%@)", currentTunnel.name)
alert.beginSheetModal(for: window) { [weak self] _ in } else {
self?.isTerminationAlertShown = true alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)", currentTunnel.name)
NSApp.terminate(nil) }
} NSApp.activate(ignoringOtherApps: true)
return .terminateCancel if let manageWindow = manageTunnelsWindowObject {
alert.beginSheetModal(for: manageWindow) { _ in }
} else { } else {
alert.runModal() alert.runModal()
return .terminateNow
} }
return .terminateCancel
}
func applicationShouldTerminateAfterLastWindowClosed(_ application: NSApplication) -> Bool {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
self?.setDockIconAndMainMenuVisibility(isVisible: false)
}
return false
}
private func setDockIconAndMainMenuVisibility(isVisible: Bool, completion: (() -> Void)? = nil) {
let currentActivationPolicy = NSApp.activationPolicy()
let newActivationPolicy: NSApplication.ActivationPolicy = isVisible ? .regular : .accessory
guard currentActivationPolicy != newActivationPolicy else {
if newActivationPolicy == .regular {
NSApp.activate(ignoringOtherApps: true)
}
completion?()
return
}
if newActivationPolicy == .regular && NSApp.isActive {
// To workaround a possible AppKit bug that causes the main menu to become unresponsive,
// we should deactivate the app first and then set the activation policy.
// NSApp.deactivate() doesn't always deactivate the app, so we instead use
// setActivationPolicy(.prohibited).
onAppDeactivation = {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
completion?()
}
NSApp.setActivationPolicy(.prohibited)
} else {
NSApp.setActivationPolicy(newActivationPolicy)
if newActivationPolicy == .regular {
NSApp.activate(ignoringOtherApps: true)
}
completion?()
}
}
func applicationDidResignActive(_ notification: Notification) {
onAppDeactivation?()
onAppDeactivation = nil
}
}
extension AppDelegate {
@objc func aboutClicked() {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
}
let appVersionString = [
tr(format: "macAppVersion (%@)", appVersion),
tr(format: "macGoBackendVersion (%@)", WIREGUARD_GO_VERSION)
].joined(separator: "\n")
let donateString = NSMutableAttributedString(string: tr("donateLink"))
donateString.addAttribute(.link, value: "https://www.wireguard.com/donations/", range: NSRange(location: 0, length: donateString.length))
NSApp.activate(ignoringOtherApps: true)
NSApp.orderFrontStandardAboutPanel(options: [
.applicationVersion: appVersionString,
.version: "",
.credits: donateString
])
} }
} }
extension AppDelegate: StatusMenuWindowDelegate { extension AppDelegate: StatusMenuWindowDelegate {
func manageTunnelsWindow() -> NSWindow { func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?) {
guard let tunnelsManager = tunnelsManager else {
completion?(nil)
return
}
if manageTunnelsWindowObject == nil { if manageTunnelsWindowObject == nil {
manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager!) manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
let window = NSWindow(contentViewController: manageTunnelsRootVC!) let window = NSWindow(contentViewController: manageTunnelsRootVC!)
window.title = tr("macWindowTitleManageTunnels") window.title = tr("macWindowTitleManageTunnels")
window.setContentSize(NSSize(width: 800, height: 480)) window.setContentSize(NSSize(width: 800, height: 480))
@@ -76,6 +236,16 @@ extension AppDelegate: StatusMenuWindowDelegate {
manageTunnelsWindowObject = window manageTunnelsWindowObject = window
tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
} }
return manageTunnelsWindowObject! setDockIconAndMainMenuVisibility(isVisible: true) { [weak manageTunnelsWindowObject] in
manageTunnelsWindowObject?.makeKeyAndOrderFront(self)
completion?(manageTunnelsWindowObject)
}
} }
} }
@discardableResult
func registerLoginItem(shouldLaunchAtLogin: Bool) -> Bool {
let appId = Bundle.main.bundleIdentifier!
let helperBundleId = "\(appId).login-item-helper"
return SMLoginItemSetEnabled(helperBundleId as CFString, shouldLaunchAtLogin)
}
@@ -5,18 +5,6 @@ import Cocoa
class Application: NSApplication { class Application: NSApplication {
private let characterKeyCommands = [
"x": #selector(NSText.cut(_:)),
"c": #selector(NSText.copy(_:)),
"v": #selector(NSText.paste(_:)),
"z": #selector(UndoActionRespondable.undo(_:)),
"a": #selector(NSResponder.selectAll(_:)),
"Z": #selector(UndoActionRespondable.redo(_:)),
"w": #selector(NSWindow.performClose(_:)),
"m": #selector(NSWindow.performMiniaturize(_:)),
"q": #selector(NSApplication.terminate(_:))
]
private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate
override init() { override init() {
@@ -28,21 +16,4 @@ class Application: NSApplication {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
override func sendEvent(_ event: NSEvent) {
let modifierFlags = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue
if event.type == .keyDown,
(modifierFlags == NSEvent.ModifierFlags.command.rawValue || modifierFlags == NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue),
let selector = characterKeyCommands[event.charactersIgnoringModifiers ?? ""] {
sendAction(selector, to: nil, from: self)
} else {
super.sendEvent(event)
}
}
}
@objc protocol UndoActionRespondable {
func undo(_ sender: AnyObject)
func redo(_ sender: AnyObject)
} }
@@ -65,4 +65,4 @@
"version" : 1, "version" : 1,
"author" : "xcode" "author" : "xcode"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -9,11 +9,11 @@ class ImportPanelPresenter {
let openPanel = NSOpenPanel() let openPanel = NSOpenPanel()
openPanel.prompt = tr("macSheetButtonImport") openPanel.prompt = tr("macSheetButtonImport")
openPanel.allowedFileTypes = ["conf", "zip"] openPanel.allowedFileTypes = ["conf", "zip"]
openPanel.allowsMultipleSelection = true
openPanel.beginSheetModal(for: window) { [weak tunnelsManager] response in openPanel.beginSheetModal(for: window) { [weak tunnelsManager] response in
guard let tunnelsManager = tunnelsManager else { return } guard let tunnelsManager = tunnelsManager else { return }
guard response == .OK else { return } guard response == .OK else { return }
guard let url = openPanel.url else { return } TunnelImporter.importFromFile(urls: openPanel.urls, into: tunnelsManager, sourceVC: sourceVC, errorPresenterType: ErrorPresenter.self)
TunnelImporter.importFromFile(url: url, into: tunnelsManager, sourceVC: sourceVC, errorPresenterType: ErrorPresenter.self)
} }
} }
} }
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMultipleInstancesProhibited</key> <key>LSMultipleInstancesProhibited</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class LaunchedAtLoginDetector {
static let launchCode = "LaunchedByWireGuardLoginItemHelper"
static func isLaunchedAtLogin(openAppleEvent: NSAppleEventDescriptor) -> Bool {
guard isOpenEvent(openAppleEvent) else { return false }
guard let propData = openAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
return propData.stringValue == launchCode
}
static func isReopenedByLoginItemHelper(reopenAppleEvent: NSAppleEventDescriptor) -> Bool {
guard isReopenEvent(reopenAppleEvent) else { return false }
guard let propData = reopenAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
return propData.stringValue == launchCode
}
}
private func isOpenEvent(_ event: NSAppleEventDescriptor) -> Bool {
return event.eventClass == kCoreEventClass && event.eventID == kAEOpenApplication
}
private func isReopenEvent(_ event: NSAppleEventDescriptor) -> Bool {
return event.eventClass == kCoreEventClass && event.eventID == kAEReopenApplication
}
@@ -0,0 +1,36 @@
<?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>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(VERSION_NAME)</string>
<key>CFBundleVersion</key>
<string>$(VERSION_ID)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>LSBackgroundOnly</key>
<true/>
<key>com.wireguard.macos.app_id</key>
<string>$(APP_ID_MACOS)</string>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?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>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
#import <Cocoa/Cocoa.h>
int main(int argc, char *argv[])
{
NSString *appIdInfoDictionaryKey = @"com.wireguard.macos.app_id";
NSString *appId = [NSBundle.mainBundle objectForInfoDictionaryKey:appIdInfoDictionaryKey];
NSString *launchCode = @"LaunchedByWireGuardLoginItemHelper";
NSAppleEventDescriptor *paramDescriptor = [NSAppleEventDescriptor descriptorWithString:launchCode];
[NSWorkspace.sharedWorkspace launchAppWithBundleIdentifier:appId options:NSWorkspaceLaunchWithoutActivation
additionalEventParamDescriptor:paramDescriptor launchIdentifier:NULL];
return 0;
}
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class MacAppStoreUpdateDetector {
static func isUpdatingFromMacAppStore(quitAppleEvent: NSAppleEventDescriptor) -> Bool {
guard isQuitEvent(quitAppleEvent) else { return false }
guard let senderPIDDescriptor = quitAppleEvent.attributeDescriptor(forKeyword: keySenderPIDAttr) else { return false }
let pid = senderPIDDescriptor.int32Value
guard let executablePath = getExecutablePath(from: pid) else { return false }
wg_log(.debug, message: "aevt/quit Apple event received from: \(executablePath)")
if executablePath.hasPrefix("/System/Library/") {
let executableName = URL(fileURLWithPath: executablePath, isDirectory: false).lastPathComponent
return executableName == "com.apple.CommerceKit.StoreAEService"
}
return false
}
}
private func isQuitEvent(_ event: NSAppleEventDescriptor) -> Bool {
return event.eventClass == kCoreEventClass && event.eventID == kAEQuitApplication
}
private func getExecutablePath(from pid: pid_t) -> String? {
let bufferSize = Int(PATH_MAX)
var buffer = Data(capacity: bufferSize)
return buffer.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) -> String? in
if let basePtr = ptr.baseAddress {
let byteCount = proc_pidpath(pid, basePtr, UInt32(bufferSize))
return byteCount > 0 ? String(cString: basePtr.bindMemory(to: CChar.self, capacity: bufferSize)) : nil
}
return nil
}
}
+130
View File
@@ -0,0 +1,130 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018 WireGuard LLC. All Rights Reserved.
import Cocoa
// swiftlint:disable colon
class MainMenu: NSMenu {
init() {
super.init(title: "")
addSubmenu(createApplicationMenu())
addSubmenu(createFileMenu())
addSubmenu(createEditMenu())
addSubmenu(createTunnelMenu())
addSubmenu(createWindowMenu())
}
required init(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func addSubmenu(_ menu: NSMenu) {
let menuItem = self.addItem(withTitle: "", action: nil, keyEquivalent: "")
self.setSubmenu(menu, for: menuItem)
}
private func createApplicationMenu() -> NSMenu {
let menu = NSMenu()
let aboutMenuItem = menu.addItem(withTitle: tr("macMenuAbout"),
action: #selector(AppDelegate.aboutClicked), keyEquivalent: "")
aboutMenuItem.target = NSApp.delegate
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuViewLog"),
action: #selector(TunnelsListTableViewController.handleViewLogAction), keyEquivalent: "")
menu.addItem(NSMenuItem.separator())
let hideMenuItem = menu.addItem(withTitle: tr("macMenuHideApp"),
action: #selector(NSApplication.hide), keyEquivalent: "h")
hideMenuItem.target = NSApp
let hideOthersMenuItem = menu.addItem(withTitle: tr("macMenuHideOtherApps"),
action: #selector(NSApplication.hideOtherApplications), keyEquivalent: "h")
hideOthersMenuItem.keyEquivalentModifierMask = [.command, .option]
hideOthersMenuItem.target = NSApp
let showAllMenuItem = menu.addItem(withTitle: tr("macMenuShowAllApps"),
action: #selector(NSApplication.unhideAllApplications), keyEquivalent: "")
showAllMenuItem.target = NSApp
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuQuit"),
action: #selector(AppDelegate.confirmAndQuit), keyEquivalent: "q")
return menu
}
private func createFileMenu() -> NSMenu {
let menu = NSMenu(title: tr("macMenuFile"))
menu.addItem(withTitle: tr("macMenuAddEmptyTunnel"),
action: #selector(TunnelsListTableViewController.handleAddEmptyTunnelAction), keyEquivalent: "n")
menu.addItem(withTitle: tr("macMenuImportTunnels"),
action: #selector(TunnelsListTableViewController.handleImportTunnelAction), keyEquivalent: "o")
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuExportTunnels"),
action: #selector(TunnelsListTableViewController.handleExportTunnelsAction), keyEquivalent: "")
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuCloseWindow"), action: #selector(NSWindow.performClose(_:)), keyEquivalent:"w")
return menu
}
private func createEditMenu() -> NSMenu {
let menu = NSMenu(title: tr("macMenuEdit"))
menu.addItem(withTitle: "", action: #selector(UndoActionRespondable.undo(_:)), keyEquivalent:"z")
menu.addItem(withTitle: "", action: #selector(UndoActionRespondable.redo(_:)), keyEquivalent:"Z")
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuCut"), action: #selector(NSText.cut(_:)), keyEquivalent:"x")
menu.addItem(withTitle: tr("macMenuCopy"), action: #selector(NSText.copy(_:)), keyEquivalent:"c")
menu.addItem(withTitle: tr("macMenuPaste"), action: #selector(NSText.paste(_:)), keyEquivalent:"v")
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuSelectAll"), action: #selector(NSText.selectAll(_:)), keyEquivalent:"a")
return menu
}
private func createTunnelMenu() -> NSMenu {
let menu = NSMenu(title: tr("macMenuTunnel"))
menu.addItem(withTitle: tr("macMenuToggleStatus"), action: #selector(TunnelDetailTableViewController.handleToggleActiveStatusAction), keyEquivalent:"t")
menu.addItem(NSMenuItem.separator())
menu.addItem(withTitle: tr("macMenuEditTunnel"), action: #selector(TunnelDetailTableViewController.handleEditTunnelAction), keyEquivalent:"e")
menu.addItem(withTitle: tr("macMenuDeleteSelected"), action: #selector(TunnelsListTableViewController.handleRemoveTunnelAction), keyEquivalent: "")
return menu
}
private func createWindowMenu() -> NSMenu {
let menu = NSMenu(title: tr("macMenuWindow"))
menu.addItem(withTitle: tr("macMenuMinimize"), action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent:"m")
menu.addItem(withTitle: tr("macMenuZoom"), action: #selector(NSWindow.performZoom(_:)), keyEquivalent:"")
menu.addItem(NSMenuItem.separator())
let fullScreenMenuItem = menu.addItem(withTitle: "", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent:"f")
fullScreenMenuItem.keyEquivalentModifierMask = [.command, .control]
return menu
}
}
@objc protocol UndoActionRespondable {
func undo(_ sender: AnyObject)
func redo(_ sender: AnyObject)
}
+27 -28
View File
@@ -4,7 +4,7 @@
import Cocoa import Cocoa
protocol StatusMenuWindowDelegate: class { protocol StatusMenuWindowDelegate: class {
func manageTunnelsWindow() -> NSWindow func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?)
} }
class StatusMenu: NSMenu { class StatusMenu: NSMenu {
@@ -13,6 +13,7 @@ class StatusMenu: NSMenu {
var statusMenuItem: NSMenuItem? var statusMenuItem: NSMenuItem?
var networksMenuItem: NSMenuItem? var networksMenuItem: NSMenuItem?
var deactivateMenuItem: NSMenuItem?
var firstTunnelMenuItemIndex = 0 var firstTunnelMenuItemIndex = 0
var numberOfTunnelMenuItems = 0 var numberOfTunnelMenuItems = 0
@@ -53,16 +54,22 @@ class StatusMenu: NSMenu {
networksMenuItem.isEnabled = false networksMenuItem.isEnabled = false
networksMenuItem.isHidden = true networksMenuItem.isHidden = true
addItem(networksMenuItem) addItem(networksMenuItem)
let deactivateMenuItem = NSMenuItem(title: tr("macToggleStatusButtonDeactivate"), action: #selector(deactivateClicked), keyEquivalent: "")
deactivateMenuItem.target = self
deactivateMenuItem.isHidden = true
addItem(deactivateMenuItem)
self.statusMenuItem = statusMenuItem self.statusMenuItem = statusMenuItem
self.networksMenuItem = networksMenuItem self.networksMenuItem = networksMenuItem
self.deactivateMenuItem = deactivateMenuItem
} }
func updateStatusMenuItems(with tunnel: TunnelContainer?) { func updateStatusMenuItems(with tunnel: TunnelContainer?) {
guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem else { return } guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem, let deactivateMenuItem = deactivateMenuItem else { return }
guard let tunnel = tunnel else { guard let tunnel = tunnel else {
statusMenuItem.title = tr(format: "macStatus (%@)", tr("tunnelStatusInactive")) statusMenuItem.title = tr(format: "macStatus (%@)", tr("tunnelStatusInactive"))
networksMenuItem.title = "" networksMenuItem.title = ""
networksMenuItem.isHidden = true networksMenuItem.isHidden = true
deactivateMenuItem.isHidden = true
return return
} }
var statusText: String var statusText: String
@@ -98,6 +105,7 @@ class StatusMenu: NSMenu {
} }
networksMenuItem.isHidden = false networksMenuItem.isHidden = false
} }
deactivateMenuItem.isHidden = tunnel.status != .active
} }
func addTunnelMenuItems() -> Bool { func addTunnelMenuItems() -> Bool {
@@ -119,14 +127,20 @@ class StatusMenu: NSMenu {
} }
func addApplicationItems() { func addApplicationItems() {
let aboutItem = NSMenuItem(title: tr("macMenuAbout"), action: #selector(aboutClicked), keyEquivalent: "") let aboutItem = NSMenuItem(title: tr("macMenuAbout"), action: #selector(AppDelegate.aboutClicked), keyEquivalent: "")
aboutItem.target = self aboutItem.target = NSApp.delegate
addItem(aboutItem) addItem(aboutItem)
let quitItem = NSMenuItem(title: tr("macMenuQuit"), action: #selector(NSApplication.terminate), keyEquivalent: "") let quitItem = NSMenuItem(title: tr("macMenuQuit"), action: #selector(AppDelegate.quit), keyEquivalent: "")
quitItem.target = NSApp quitItem.target = NSApp.delegate
addItem(quitItem) addItem(quitItem)
} }
@objc func deactivateClicked() {
if let currentTunnel = currentTunnel {
tunnelsManager.startDeactivation(of: currentTunnel)
}
}
@objc func tunnelClicked(sender: AnyObject) { @objc func tunnelClicked(sender: AnyObject) {
guard let tunnelMenuItem = sender as? TunnelMenuItem else { return } guard let tunnelMenuItem = sender as? TunnelMenuItem else { return }
if tunnelMenuItem.state == .off { if tunnelMenuItem.state == .off {
@@ -137,32 +151,16 @@ class StatusMenu: NSMenu {
} }
@objc func manageTunnelsClicked() { @objc func manageTunnelsClicked() {
NSApp.activate(ignoringOtherApps: true) windowDelegate?.showManageTunnelsWindow(completion: nil)
guard let manageTunnelsWindow = windowDelegate?.manageTunnelsWindow() else { return }
manageTunnelsWindow.makeKeyAndOrderFront(self)
} }
@objc func importTunnelsClicked() { @objc func importTunnelsClicked() {
NSApp.activate(ignoringOtherApps: true) windowDelegate?.showManageTunnelsWindow { [weak self] manageTunnelsWindow in
guard let manageTunnelsWindow = windowDelegate?.manageTunnelsWindow() else { return } guard let self = self else { return }
manageTunnelsWindow.makeKeyAndOrderFront(self) guard let manageTunnelsWindow = manageTunnelsWindow else { return }
ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: manageTunnelsWindow.contentViewController) ImportPanelPresenter.presentImportPanel(tunnelsManager: self.tunnelsManager,
} sourceVC: manageTunnelsWindow.contentViewController)
@objc func aboutClicked() {
var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
appVersion += " (\(appBuild))"
} }
let appVersionString = [
tr(format: "macAppVersion (%@)", appVersion),
tr(format: "macGoBackendVersion (%@)", WIREGUARD_GO_VERSION)
].joined(separator: "\n")
NSApp.activate(ignoringOtherApps: true)
NSApp.orderFrontStandardAboutPanel(options: [
.applicationVersion: appVersionString,
.version: ""
])
} }
} }
@@ -170,6 +168,7 @@ extension StatusMenu {
func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) { func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:))) let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
menuItem.target = self menuItem.target = self
menuItem.isHidden = !tunnel.isTunnelAvailableToUser
insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex) insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
if numberOfTunnelMenuItems == 0 { if numberOfTunnelMenuItems == 0 {
insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1) insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class ButtonRow: NSView {
let button: NSButton = {
let button = NSButton()
button.title = ""
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
var buttonTitle: String {
get { return button.title }
set(value) { button.title = value }
}
var isButtonEnabled: Bool {
get { return button.isEnabled }
set(value) { button.isEnabled = value }
}
var buttonToolTip: String {
get { return button.toolTip ?? "" }
set(value) { button.toolTip = value }
}
var onButtonClicked: (() -> Void)?
var observationToken: AnyObject?
override var intrinsicContentSize: NSSize {
return NSSize(width: NSView.noIntrinsicMetric, height: button.intrinsicContentSize.height)
}
init() {
super.init(frame: CGRect.zero)
button.target = self
button.action = #selector(buttonClicked)
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 155),
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
])
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func buttonClicked() {
onButtonClicked?()
}
override func prepareForReuse() {
buttonTitle = ""
buttonToolTip = ""
onButtonClicked = nil
observationToken = nil
}
}
@@ -6,7 +6,6 @@ import Cocoa
private let fontSize: CGFloat = 15 private let fontSize: CGFloat = 15
class ConfTextStorage: NSTextStorage { class ConfTextStorage: NSTextStorage {
let defaultFont = NSFontManager.shared.convertWeight(true, of: NSFont.systemFont(ofSize: fontSize)) let defaultFont = NSFontManager.shared.convertWeight(true, of: NSFont.systemFont(ofSize: fontSize))
private let boldFont = NSFont.boldSystemFont(ofSize: fontSize) private let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
private lazy var italicFont = NSFontManager.shared.convert(defaultFont, toHaveTrait: .italicFontMask) private lazy var italicFont = NSFontManager.shared.convert(defaultFont, toHaveTrait: .italicFontMask)
@@ -17,6 +16,11 @@ class ConfTextStorage: NSTextStorage {
private(set) var hasError = false private(set) var hasError = false
private(set) var privateKeyString: String? private(set) var privateKeyString: String?
private(set) var hasOnePeer: Bool = false
private(set) var lastOnePeerAllowedIPs = [String]()
private(set) var lastOnePeerDNSServers = [String]()
private(set) var lastOnePeerHasPublicKey = false
override init() { override init() {
backingStore = NSMutableAttributedString(string: "") backingStore = NSMutableAttributedString(string: "")
super.init() super.init()
@@ -81,6 +85,55 @@ class ConfTextStorage: NSTextStorage {
endEditing() endEditing()
} }
func resetLastPeer() {
hasOnePeer = false
lastOnePeerAllowedIPs = []
lastOnePeerDNSServers = []
lastOnePeerHasPublicKey = false
}
func evaluateExcludePrivateIPs(highlightSpans: UnsafePointer<highlight_span>) {
var spans = highlightSpans
enum FieldType: String {
case dns
case allowedips
}
var fieldType: FieldType?
resetLastPeer()
while spans.pointee.type != HighlightEnd {
let span = spans.pointee
var substring = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string.lowercased()
if span.type == HighlightError {
resetLastPeer()
return
} else if span.type == HighlightSection {
if substring == "[peer]" {
if hasOnePeer {
resetLastPeer()
return
}
hasOnePeer = true
}
} else if span.type == HighlightField {
fieldType = FieldType(rawValue: substring)
} else if span.type == HighlightIP && fieldType == .dns {
lastOnePeerDNSServers.append(substring)
} else if span.type == HighlightIP && fieldType == .allowedips {
let next = spans.successor()
let nextnext = next.successor()
if next.pointee.type == HighlightDelimiter && nextnext.pointee.type == HighlightCidr {
substring += backingStore.attributedSubstring(from: NSRange(location: next.pointee.start, length: next.pointee.len)).string +
backingStore.attributedSubstring(from: NSRange(location: nextnext.pointee.start, length: nextnext.pointee.len)).string
}
lastOnePeerAllowedIPs.append(substring)
} else if span.type == HighlightPublicKey {
lastOnePeerHasPublicKey = true
}
spans = spans.successor()
}
}
func highlightSyntax() { func highlightSyntax() {
guard let textColorTheme = textColorTheme else { return } guard let textColorTheme = textColorTheme else { return }
hasError = false hasError = false
@@ -94,8 +147,10 @@ class ConfTextStorage: NSTextStorage {
.font: defaultFont .font: defaultFont
] ]
backingStore.setAttributes(defaultAttributes, range: fullTextRange) backingStore.setAttributes(defaultAttributes, range: fullTextRange)
var spans = highlight_config(backingStore.string.cString(using: String.Encoding.utf8))! var spans = highlight_config(backingStore.string)!
evaluateExcludePrivateIPs(highlightSpans: spans)
let spansStart = spans
while spans.pointee.type != HighlightEnd { while spans.pointee.type != HighlightEnd {
let span = spans.pointee let span = spans.pointee
@@ -115,6 +170,7 @@ class ConfTextStorage: NSTextStorage {
spans = spans.successor() spans = spans.successor()
} }
backingStore.endEditing() backingStore.endEditing()
free(spansStart)
beginEditing() beginEditing()
edited(.editedAttributes, range: fullTextRange, changeInLength: 0) edited(.editedAttributes, range: fullTextRange, changeInLength: 0)
@@ -9,10 +9,12 @@ class ConfTextView: NSTextView {
@objc dynamic var hasError: Bool = false @objc dynamic var hasError: Bool = false
@objc dynamic var privateKeyString: String? @objc dynamic var privateKeyString: String?
@objc dynamic var singlePeerAllowedIPs: [String]?
override var string: String { override var string: String {
didSet { didSet {
confTextStorage.highlightSyntax() confTextStorage.highlightSyntax()
updateConfigData()
} }
} }
@@ -52,18 +54,35 @@ class ConfTextView: NSTextView {
} }
} }
} private func updateConfigData() {
extension ConfTextView: NSTextViewDelegate {
func textDidChange(_ notification: Notification) {
confTextStorage.highlightSyntax()
if hasError != confTextStorage.hasError { if hasError != confTextStorage.hasError {
hasError = confTextStorage.hasError hasError = confTextStorage.hasError
} }
if privateKeyString != confTextStorage.privateKeyString { if privateKeyString != confTextStorage.privateKeyString {
privateKeyString = confTextStorage.privateKeyString privateKeyString = confTextStorage.privateKeyString
} }
let hasSyntaxError = confTextStorage.hasError
let hasSemanticError = confTextStorage.privateKeyString == nil || !confTextStorage.lastOnePeerHasPublicKey
let updatedSinglePeerAllowedIPs = confTextStorage.hasOnePeer && !hasSyntaxError && !hasSemanticError ? confTextStorage.lastOnePeerAllowedIPs : nil
if singlePeerAllowedIPs != updatedSinglePeerAllowedIPs {
singlePeerAllowedIPs = updatedSinglePeerAllowedIPs
}
}
func setConfText(_ text: String) {
let fullTextRange = NSRange(location: 0, length: (string as NSString).length)
if shouldChangeText(in: fullTextRange, replacementString: text) {
replaceCharacters(in: fullTextRange, with: text)
didChangeText()
}
}
}
extension ConfTextView: NSTextViewDelegate {
func textDidChange(_ notification: Notification) {
confTextStorage.highlightSyntax()
updateConfigData()
needsDisplay = true needsDisplay = true
} }
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class DeleteTunnelsConfirmationAlert: NSAlert {
var alertDeleteButton: NSButton?
var alertCancelButton: NSButton?
var onDeleteClicked: ((_ completionHandler: @escaping () -> Void) -> Void)?
override init() {
super.init()
let alertDeleteButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleDelete"))
alertDeleteButton.target = self
alertDeleteButton.action = #selector(removeTunnelAlertDeleteClicked)
self.alertDeleteButton = alertDeleteButton
self.alertCancelButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleCancel"))
}
@objc func removeTunnelAlertDeleteClicked() {
alertDeleteButton?.title = tr("macDeleteTunnelConfirmationAlertButtonTitleDeleting")
alertDeleteButton?.isEnabled = false
alertCancelButton?.isEnabled = false
if let onDeleteClicked = onDeleteClicked {
onDeleteClicked { [weak self] in
guard let self = self else { return }
self.window.sheetParent?.endSheet(self.window)
}
}
}
func beginSheetModal(for sheetWindow: NSWindow) {
beginSheetModal(for: sheetWindow) { _ in }
}
}
@@ -24,6 +24,8 @@ class EditableKeyValueRow: NSView {
return valueLabel return valueLabel
}() }()
let valueImageView: NSImageView?
var key: String { var key: String {
get { return keyLabel.stringValue } get { return keyLabel.stringValue }
set(value) { keyLabel.stringValue = value } set(value) { keyLabel.stringValue = value }
@@ -42,13 +44,24 @@ class EditableKeyValueRow: NSView {
} }
} }
} }
var valueImage: NSImage? {
get { return valueImageView?.image }
set(value) { valueImageView?.image = value }
}
var observationToken: AnyObject?
override var intrinsicContentSize: NSSize { override var intrinsicContentSize: NSSize {
let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height) let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height)
return NSSize(width: NSView.noIntrinsicMetric, height: height) return NSSize(width: NSView.noIntrinsicMetric, height: height)
} }
init() { convenience init() {
self.init(hasValueImage: false)
}
fileprivate init(hasValueImage: Bool) {
valueImageView = hasValueImage ? NSImageView() : nil
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
addSubview(keyLabel) addSubview(keyLabel)
@@ -60,10 +73,24 @@ class EditableKeyValueRow: NSView {
keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
keyLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor), keyLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor),
self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor), self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: valueLabel.leadingAnchor, constant: -5),
valueLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor) valueLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)
]) ])
let spacing: CGFloat = 5
if let valueImageView = valueImageView {
addSubview(valueImageView)
valueImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
valueImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
valueImageView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing),
valueLabel.leadingAnchor.constraint(equalTo: valueImageView.trailingAnchor)
])
} else {
NSLayoutConstraint.activate([
valueLabel.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing)
])
}
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal) keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
@@ -81,12 +108,26 @@ class EditableKeyValueRow: NSView {
key = "" key = ""
value = "" value = ""
isKeyInBold = false isKeyInBold = false
observationToken = nil
} }
} }
class KeyValueRow: EditableKeyValueRow { class KeyValueRow: EditableKeyValueRow {
override init() { init() {
super.init() super.init(hasValueImage: false)
valueLabel.isEditable = false
valueLabel.isBordered = false
valueLabel.backgroundColor = .clear
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class KeyValueImageRow: EditableKeyValueRow {
init() {
super.init(hasValueImage: true)
valueLabel.isEditable = false valueLabel.isEditable = false
valueLabel.isBordered = false valueLabel.isBordered = false
valueLabel.backgroundColor = .clear valueLabel.backgroundColor = .clear
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class LogViewCell: NSTableCellView {
var text: String = "" {
didSet { textField?.stringValue = text }
}
init() {
super.init(frame: .zero)
let textField = NSTextField(wrappingLabelWithString: "")
addSubview(textField)
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: self.leadingAnchor),
textField.trailingAnchor.constraint(equalTo: self.trailingAnchor),
textField.topAnchor.constraint(equalTo: self.topAnchor),
textField.bottomAnchor.constraint(equalTo: self.bottomAnchor)
])
self.textField = textField
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
textField?.stringValue = ""
}
}
class LogViewTimestampCell: LogViewCell {
override init() {
super.init()
if let textField = textField {
textField.maximumNumberOfLines = 1
textField.lineBreakMode = .byClipping
textField.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultLow, for: .vertical)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class LogViewMessageCell: LogViewCell {
override init() {
super.init()
if let textField = textField {
textField.maximumNumberOfLines = 0
textField.lineBreakMode = .byWordWrapping
textField.setContentCompressionResistancePriority(.required, for: .vertical)
textField.setContentHuggingPriority(.required, for: .vertical)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
import CoreWLAN
class OnDemandControlsRow: NSView {
let keyLabel: NSTextField = {
let keyLabel = NSTextField()
keyLabel.stringValue = tr("macFieldOnDemand")
keyLabel.isEditable = false
keyLabel.isSelectable = false
keyLabel.isBordered = false
keyLabel.alignment = .right
keyLabel.maximumNumberOfLines = 1
keyLabel.lineBreakMode = .byTruncatingTail
keyLabel.backgroundColor = .clear
return keyLabel
}()
let onDemandEthernetCheckbox: NSButton = {
let checkbox = NSButton()
checkbox.title = tr("tunnelOnDemandEthernet")
checkbox.setButtonType(.switch)
checkbox.state = .off
return checkbox
}()
let onDemandWiFiCheckbox: NSButton = {
let checkbox = NSButton()
checkbox.title = tr("tunnelOnDemandWiFi")
checkbox.setButtonType(.switch)
checkbox.state = .off
return checkbox
}()
static let onDemandSSIDOptions: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
.anySSID, .onlySpecificSSIDs, .exceptSpecificSSIDs
]
let onDemandSSIDOptionsPopup = NSPopUpButton()
let onDemandSSIDsField: NSTokenField = {
let tokenField = NSTokenField()
tokenField.tokenizingCharacterSet = CharacterSet([])
tokenField.tokenStyle = .squared
NSLayoutConstraint.activate([
tokenField.widthAnchor.constraint(greaterThanOrEqualToConstant: 180)
])
return tokenField
}()
override var intrinsicContentSize: NSSize {
let minHeight: CGFloat = 22
let height = max(minHeight, keyLabel.intrinsicContentSize.height,
onDemandEthernetCheckbox.intrinsicContentSize.height, onDemandWiFiCheckbox.intrinsicContentSize.height,
onDemandSSIDOptionsPopup.intrinsicContentSize.height, onDemandSSIDsField.intrinsicContentSize.height)
return NSSize(width: NSView.noIntrinsicMetric, height: height)
}
var onDemandViewModel: ActivateOnDemandViewModel? {
didSet { updateControls() }
}
var currentSSIDs: [String]
init() {
currentSSIDs = getCurrentSSIDs()
super.init(frame: CGRect.zero)
onDemandSSIDOptionsPopup.addItems(withTitles: OnDemandControlsRow.onDemandSSIDOptions.map { $0.localizedUIString })
let stackView = NSStackView()
stackView.setViews([onDemandEthernetCheckbox, onDemandWiFiCheckbox, onDemandSSIDOptionsPopup, onDemandSSIDsField], in: .leading)
stackView.orientation = .horizontal
addSubview(keyLabel)
addSubview(stackView)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
stackView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: 5),
stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
widthConstraint.priority = .defaultHigh + 1
widthConstraint.isActive = true
NSLayoutConstraint.activate([
onDemandEthernetCheckbox.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
onDemandWiFiCheckbox.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
onDemandSSIDOptionsPopup.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
onDemandSSIDsField.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor)
])
onDemandSSIDsField.setContentHuggingPriority(.defaultLow, for: .horizontal)
onDemandEthernetCheckbox.target = self
onDemandEthernetCheckbox.action = #selector(ethernetCheckboxToggled)
onDemandWiFiCheckbox.target = self
onDemandWiFiCheckbox.action = #selector(wiFiCheckboxToggled)
onDemandSSIDOptionsPopup.target = self
onDemandSSIDOptionsPopup.action = #selector(ssidOptionsPopupValueChanged)
onDemandSSIDsField.delegate = self
updateControls()
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func saveToViewModel() {
guard let onDemandViewModel = onDemandViewModel else { return }
onDemandViewModel.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
onDemandViewModel.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
onDemandViewModel.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[onDemandSSIDOptionsPopup.indexOfSelectedItem]
onDemandViewModel.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
}
func updateControls() {
guard let onDemandViewModel = onDemandViewModel else { return }
onDemandEthernetCheckbox.state = onDemandViewModel.isNonWiFiInterfaceEnabled ? .on : .off
onDemandWiFiCheckbox.state = onDemandViewModel.isWiFiInterfaceEnabled ? .on : .off
let optionIndex = OnDemandControlsRow.onDemandSSIDOptions.firstIndex(of: onDemandViewModel.ssidOption)
onDemandSSIDOptionsPopup.selectItem(at: optionIndex ?? 0)
onDemandSSIDsField.objectValue = onDemandViewModel.selectedSSIDs
onDemandSSIDOptionsPopup.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled
onDemandSSIDsField.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled || onDemandViewModel.ssidOption == .anySSID
}
@objc func ethernetCheckboxToggled() {
onDemandViewModel?.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
}
@objc func wiFiCheckboxToggled() {
onDemandViewModel?.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
updateControls()
}
@objc func ssidOptionsPopupValueChanged() {
let selectedIndex = onDemandSSIDOptionsPopup.indexOfSelectedItem
onDemandViewModel?.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[selectedIndex]
onDemandViewModel?.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
updateControls()
if !onDemandSSIDsField.isHidden {
onDemandSSIDsField.becomeFirstResponder()
}
}
}
extension OnDemandControlsRow: NSTokenFieldDelegate {
func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
return currentSSIDs.filter { $0.hasPrefix(substring) }
}
}
private func getCurrentSSIDs() -> [String] {
return CWWiFiClient.shared().interfaces()?.compactMap { $0.ssid() } ?? []
}
@@ -1,75 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class PopupRow: NSView {
let keyLabel: NSTextField = {
let keyLabel = NSTextField()
keyLabel.isEditable = false
keyLabel.isSelectable = false
keyLabel.isBordered = false
keyLabel.alignment = .right
keyLabel.maximumNumberOfLines = 1
keyLabel.lineBreakMode = .byTruncatingTail
keyLabel.backgroundColor = .clear
return keyLabel
}()
let valuePopup = NSPopUpButton()
var key: String {
get { return keyLabel.stringValue }
set(value) { keyLabel.stringValue = value }
}
var valueOptions: [String] {
get { return valuePopup.itemTitles }
set(value) {
valuePopup.removeAllItems()
valuePopup.addItems(withTitles: value)
}
}
var selectedOptionIndex: Int {
get { return valuePopup.indexOfSelectedItem }
set(value) { valuePopup.selectItem(at: value) }
}
override var intrinsicContentSize: NSSize {
let height = max(keyLabel.intrinsicContentSize.height, valuePopup.intrinsicContentSize.height)
return NSSize(width: NSView.noIntrinsicMetric, height: height)
}
init() {
super.init(frame: CGRect.zero)
addSubview(keyLabel)
addSubview(valuePopup)
keyLabel.translatesAutoresizingMaskIntoConstraints = false
valuePopup.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
keyLabel.firstBaselineAnchor.constraint(equalTo: valuePopup.firstBaselineAnchor),
self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
keyLabel.trailingAnchor.constraint(equalTo: valuePopup.leadingAnchor, constant: -5)
])
keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
widthConstraint.priority = .defaultHigh + 1
widthConstraint.isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForReuse() {
key = ""
valueOptions = []
}
}
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class ButtonedDetailViewController: NSViewController {
var onButtonClicked: (() -> Void)?
let button: NSButton = {
let button = NSButton()
button.title = ""
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let view = NSView()
button.target = self
button.action = #selector(buttonClicked)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
NSLayoutConstraint.activate([
view.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
])
self.view = view
}
func setButtonTitle(_ title: String) {
button.title = title
}
@objc func buttonClicked() {
onButtonClicked?()
}
}
@@ -0,0 +1,274 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class LogViewController: NSViewController {
enum LogColumn: String {
case time = "Time"
case logMessage = "LogMessage"
func createColumn() -> NSTableColumn {
return NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue))
}
func isRepresenting(tableColumn: NSTableColumn?) -> Bool {
return tableColumn?.identifier.rawValue == rawValue
}
}
let scrollView: NSScrollView = {
let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true
scrollView.autohidesScrollers = false
scrollView.borderType = .bezelBorder
return scrollView
}()
let tableView: NSTableView = {
let tableView = NSTableView()
let timeColumn = LogColumn.time.createColumn()
timeColumn.title = tr("macLogColumnTitleTime")
timeColumn.width = 160
timeColumn.resizingMask = []
tableView.addTableColumn(timeColumn)
let messageColumn = LogColumn.logMessage.createColumn()
messageColumn.title = tr("macLogColumnTitleLogMessage")
messageColumn.minWidth = 360
messageColumn.resizingMask = .autoresizingMask
tableView.addTableColumn(messageColumn)
tableView.rowSizeStyle = .custom
tableView.rowHeight = 16
tableView.usesAlternatingRowBackgroundColors = true
tableView.usesAutomaticRowHeights = true
tableView.allowsColumnReordering = false
tableView.allowsColumnResizing = true
tableView.allowsMultipleSelection = true
return tableView
}()
let progressIndicator: NSProgressIndicator = {
let progressIndicator = NSProgressIndicator()
progressIndicator.controlSize = .small
progressIndicator.isIndeterminate = true
progressIndicator.style = .spinning
progressIndicator.isDisplayedWhenStopped = false
return progressIndicator
}()
let closeButton: NSButton = {
let button = NSButton()
button.title = tr("macLogButtonTitleClose")
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
let saveButton: NSButton = {
let button = NSButton()
button.title = tr("macLogButtonTitleSave")
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
let logViewHelper: LogViewHelper?
var logEntries = [LogViewHelper.LogEntry]()
var isFetchingLogEntries = false
var isInScrolledToEndMode = true
private var updateLogEntriesTimer: Timer?
init() {
logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
tableView.dataSource = self
tableView.delegate = self
closeButton.target = self
closeButton.action = #selector(closeClicked)
saveButton.target = self
saveButton.action = #selector(saveClicked)
saveButton.isEnabled = false
let clipView = NSClipView()
clipView.documentView = tableView
scrollView.contentView = clipView
_ = NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: clipView, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
let lastVisibleRowIndex = self.tableView.row(at: NSPoint(x: 0, y: self.scrollView.contentView.documentVisibleRect.maxY - 1))
self.isInScrolledToEndMode = lastVisibleRowIndex < 0 || lastVisibleRowIndex == self.logEntries.count - 1
}
_ = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: tableView, queue: OperationQueue.main) { [weak self] _ in
guard let self = self else { return }
if self.isInScrolledToEndMode {
DispatchQueue.main.async {
self.tableView.scroll(NSPoint(x: 0, y: self.tableView.frame.maxY - clipView.documentVisibleRect.height))
}
}
}
let margin: CGFloat = 20
let internalSpacing: CGFloat = 10
let buttonRowStackView = NSStackView()
buttonRowStackView.addView(closeButton, in: .leading)
buttonRowStackView.addView(saveButton, in: .trailing)
buttonRowStackView.orientation = .horizontal
buttonRowStackView.spacing = internalSpacing
let containerView = NSView()
[scrollView, progressIndicator, buttonRowStackView].forEach { view in
containerView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: margin),
scrollView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
containerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: margin),
buttonRowStackView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: internalSpacing),
buttonRowStackView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
containerView.rightAnchor.constraint(equalTo: buttonRowStackView.rightAnchor, constant: margin),
containerView.bottomAnchor.constraint(equalTo: buttonRowStackView.bottomAnchor, constant: margin),
progressIndicator.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
progressIndicator.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
])
NSLayoutConstraint.activate([
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 640),
containerView.widthAnchor.constraint(lessThanOrEqualToConstant: 1200),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
])
containerView.frame = NSRect(x: 0, y: 0, width: 640, height: 480)
view = containerView
progressIndicator.startAnimation(self)
startUpdatingLogEntries()
}
func updateLogEntries() {
guard !isFetchingLogEntries else { return }
isFetchingLogEntries = true
logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in
guard let self = self else { return }
defer {
self.isFetchingLogEntries = false
}
if !self.progressIndicator.isHidden {
self.progressIndicator.stopAnimation(self)
self.saveButton.isEnabled = true
}
guard !fetchedLogEntries.isEmpty else { return }
let oldCount = self.logEntries.count
self.logEntries.append(contentsOf: fetchedLogEntries)
self.tableView.insertRows(at: IndexSet(integersIn: oldCount ..< oldCount + fetchedLogEntries.count), withAnimation: .slideDown)
}
}
func startUpdatingLogEntries() {
updateLogEntries()
updateLogEntriesTimer?.invalidate()
let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
self?.updateLogEntries()
}
updateLogEntriesTimer = timer
RunLoop.main.add(timer, forMode: .common)
}
func stopUpdatingLogEntries() {
updateLogEntriesTimer?.invalidate()
updateLogEntriesTimer = nil
}
override func viewWillAppear() {
view.window?.setFrameAutosaveName(NSWindow.FrameAutosaveName("LogWindow"))
}
override func viewWillDisappear() {
super.viewWillDisappear()
stopUpdatingLogEntries()
}
@objc func saveClicked() {
let savePanel = NSSavePanel()
savePanel.prompt = tr("macSheetButtonExportLog")
savePanel.nameFieldLabel = tr("macNameFieldExportLog")
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
let timeStampString = dateFormatter.string(from: Date())
savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
savePanel.beginSheetModal(for: self.view.window!) { [weak self] response in
guard response == .OK else { return }
guard let destinationURL = savePanel.url else { return }
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
guard isWritten else {
DispatchQueue.main.async { [weak self] in
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
}
return
}
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.presentingViewController?.dismiss(self)
}
}
}
}
@objc func closeClicked() {
presentingViewController?.dismiss(self)
}
@objc func copy(_ sender: Any?) {
let text = tableView.selectedRowIndexes.sorted().reduce("") { $0 + self.logEntries[$1].text() + "\n" }
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([text as NSString])
}
}
extension LogViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return logEntries.count
}
}
extension LogViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
cell.text = logEntries[row].timestamp
return cell
} else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
let cell: LogViewMessageCell = tableView.dequeueReusableCell()
cell.text = logEntries[row].message
return cell
} else {
fatalError()
}
}
}
extension LogViewController {
override func cancelOperation(_ sender: Any?) {
closeClicked()
}
}
@@ -54,8 +54,7 @@ class ManageTunnelsRootViewController: NSViewController {
tunnelDetailContainerView.topAnchor.constraint(equalTo: container.topAnchor), tunnelDetailContainerView.topAnchor.constraint(equalTo: container.topAnchor),
tunnelDetailContainerView.bottomAnchor.constraint(equalTo: container.bottomAnchor), tunnelDetailContainerView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
tunnelDetailContainerView.leadingAnchor.constraint(equalTo: tunnelsListView.trailingAnchor, constant: centralSpacing), tunnelDetailContainerView.leadingAnchor.constraint(equalTo: tunnelsListView.trailingAnchor, constant: centralSpacing),
tunnelDetailContainerView.trailingAnchor.constraint(equalTo: container.trailingAnchor), tunnelDetailContainerView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
tunnelsListView.widthAnchor.constraint(equalTo: container.widthAnchor, multiplier: 0.3)
]) ])
} }
@@ -78,39 +77,59 @@ class ManageTunnelsRootViewController: NSViewController {
} }
extension ManageTunnelsRootViewController: TunnelsListTableViewControllerDelegate { extension ManageTunnelsRootViewController: TunnelsListTableViewControllerDelegate {
func tunnelSelected(tunnel: TunnelContainer) { func tunnelsSelected(tunnelIndices: [Int]) {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel) assert(!tunnelIndices.isEmpty)
setTunnelDetailContentVC(tunnelDetailVC) if tunnelIndices.count == 1 {
self.tunnelDetailVC = tunnelDetailVC let tunnel = tunnelsManager.tunnel(at: tunnelIndices.first!)
if tunnel.isTunnelAvailableToUser {
let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
setTunnelDetailContentVC(tunnelDetailVC)
self.tunnelDetailVC = tunnelDetailVC
} else {
let unusableTunnelDetailVC = tunnelDetailContentVC as? UnusableTunnelDetailViewController ?? UnusableTunnelDetailViewController()
unusableTunnelDetailVC.onButtonClicked = { [weak tunnelsListVC] in
tunnelsListVC?.handleRemoveTunnelAction()
}
setTunnelDetailContentVC(unusableTunnelDetailVC)
self.tunnelDetailVC = nil
}
} else if tunnelIndices.count > 1 {
let multiSelectionVC = tunnelDetailContentVC as? ButtonedDetailViewController ?? ButtonedDetailViewController()
multiSelectionVC.setButtonTitle(tr(format: "macButtonDeleteTunnels (%d)", tunnelIndices.count))
multiSelectionVC.onButtonClicked = { [weak tunnelsListVC] in
tunnelsListVC?.handleRemoveTunnelAction()
}
setTunnelDetailContentVC(multiSelectionVC)
self.tunnelDetailVC = nil
}
} }
func tunnelsListEmpty() { func tunnelsListEmpty() {
let noTunnelsVC = NoTunnelsDetailViewController(tunnelsManager: tunnelsManager) let noTunnelsVC = ButtonedDetailViewController()
noTunnelsVC.setButtonTitle(tr("macButtonImportTunnels"))
noTunnelsVC.onButtonClicked = { [weak self] in
guard let self = self else { return }
ImportPanelPresenter.presentImportPanel(tunnelsManager: self.tunnelsManager, sourceVC: self)
}
setTunnelDetailContentVC(noTunnelsVC) setTunnelDetailContentVC(noTunnelsVC)
self.tunnelDetailVC = nil self.tunnelDetailVC = nil
} }
} }
extension ManageTunnelsRootViewController { extension ManageTunnelsRootViewController {
override func keyDown(with event: NSEvent) { override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
let modifierFlags = event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue switch action {
let isCmdOrCmdShiftDown = (modifierFlags == NSEvent.ModifierFlags.command.rawValue || modifierFlags == NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue) case #selector(TunnelsListTableViewController.handleViewLogAction),
#selector(TunnelsListTableViewController.handleAddEmptyTunnelAction),
if event.specialKey == .delete { #selector(TunnelsListTableViewController.handleImportTunnelAction),
tunnelsListVC?.handleRemoveTunnelAction() #selector(TunnelsListTableViewController.handleExportTunnelsAction),
} else if isCmdOrCmdShiftDown { #selector(TunnelsListTableViewController.handleRemoveTunnelAction):
switch event.charactersIgnoringModifiers { return tunnelsListVC
case "n": case #selector(TunnelDetailTableViewController.handleToggleActiveStatusAction),
tunnelsListVC?.handleAddEmptyTunnelAction() #selector(TunnelDetailTableViewController.handleEditTunnelAction):
case "i": return tunnelDetailVC
tunnelsListVC?.handleImportTunnelAction() default:
case "t": return super.supplementalTarget(forAction: action, sender: sender)
tunnelDetailVC?.handleToggleActiveStatusAction()
case "e":
tunnelDetailVC?.handleEditTunnelAction()
default:
break
}
} }
} }
} }
@@ -1,46 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class NoTunnelsDetailViewController: NSViewController {
let tunnelsManager: TunnelsManager
let importButton: NSButton = {
let button = NSButton()
button.title = tr("macButtonImportTunnels")
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
init(tunnelsManager: TunnelsManager) {
self.tunnelsManager = tunnelsManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
let view = NSView()
importButton.target = self
importButton.action = #selector(importTunnelClicked)
view.addSubview(importButton)
importButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
importButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
importButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
self.view = view
}
@objc func importTunnelClicked() {
// We pass sourceVC as parent instead of self because this VC will not be visible when the import completes
ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: parent)
}
}
@@ -9,13 +9,15 @@ class TunnelDetailTableViewController: NSViewController {
case interfaceFieldRow(TunnelViewModel.InterfaceField) case interfaceFieldRow(TunnelViewModel.InterfaceField)
case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField)
case onDemandRow case onDemandRow
case onDemandSSIDRow
case spacerRow case spacerRow
func localizedSectionKeyString() -> String { func localizedSectionKeyString() -> String {
switch self { switch self {
case .interfaceFieldRow: return tr("tunnelSectionTitleInterface") case .interfaceFieldRow: return tr("tunnelSectionTitleInterface")
case .peerFieldRow: return tr("tunnelSectionTitlePeer") case .peerFieldRow: return tr("tunnelSectionTitlePeer")
case .onDemandRow: return "" case .onDemandRow: return tr("macFieldOnDemand")
case .onDemandSSIDRow: return ""
case .spacerRow: return "" case .spacerRow: return ""
} }
} }
@@ -25,14 +27,15 @@ class TunnelDetailTableViewController: NSViewController {
case .interfaceFieldRow(let field): return field == .name case .interfaceFieldRow(let field): return field == .name
case .peerFieldRow(_, let field): return field == .publicKey case .peerFieldRow(_, let field): return field == .publicKey
case .onDemandRow: return true case .onDemandRow: return true
case .onDemandSSIDRow: return false
case .spacerRow: return false case .spacerRow: return false
} }
} }
} }
static let interfaceFields: [TunnelViewModel.InterfaceField] = [ static let interfaceFields: [TunnelViewModel.InterfaceField] = [
.name, .publicKey, .addresses, .name, .status, .publicKey, .addresses,
.listenPort, .mtu, .dns .listenPort, .mtu, .dns, .toggleStatus
] ]
static let peerFields: [TunnelViewModel.PeerField] = [ static let peerFields: [TunnelViewModel.PeerField] = [
@@ -41,6 +44,10 @@ class TunnelDetailTableViewController: NSViewController {
.rxBytes, .txBytes, .lastHandshakeTime .rxBytes, .txBytes, .lastHandshakeTime
] ]
static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
.onDemand, .ssid
]
let tableView: NSTableView = { let tableView: NSTableView = {
let tableView = NSTableView() let tableView = NSTableView()
tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelDetail"))) tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelDetail")))
@@ -51,21 +58,12 @@ class TunnelDetailTableViewController: NSViewController {
return tableView return tableView
}() }()
let statusCheckbox: NSButton = {
let checkbox = NSButton()
checkbox.title = ""
checkbox.setButtonType(.switch)
checkbox.state = .off
checkbox.toolTip = "Toggle status (⌘T)"
return checkbox
}()
let editButton: NSButton = { let editButton: NSButton = {
let button = NSButton() let button = NSButton()
button.title = tr("Edit") button.title = tr("Edit")
button.setButtonType(.momentaryPushIn) button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded button.bezelStyle = .rounded
button.toolTip = "Edit tunnel (⌘E)" button.toolTip = tr("macToolTipEditTunnel")
return button return button
}() }()
@@ -85,6 +83,9 @@ class TunnelDetailTableViewController: NSViewController {
updateTableViewModelRows() updateTableViewModelRows()
} }
} }
var onDemandViewModel: ActivateOnDemandViewModel
private var tableViewModelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]() private var tableViewModelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
private var tableViewModelRows = [TableViewModelRow]() private var tableViewModelRows = [TableViewModelRow]()
@@ -96,12 +97,18 @@ class TunnelDetailTableViewController: NSViewController {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
updateTableViewModelRowsBySection() updateTableViewModelRowsBySection()
updateTableViewModelRows() updateTableViewModelRows()
updateStatus()
statusObservationToken = tunnel.observe(\TunnelContainer.status) { [weak self] _, _ in statusObservationToken = tunnel.observe(\TunnelContainer.status) { [weak self] _, _ in
self?.updateStatus() guard let self = self else { return }
if tunnel.status == .active {
self.startUpdatingRuntimeConfiguration()
} else if tunnel.status == .inactive {
self.reloadRuntimeConfiguration()
self.stopUpdatingRuntimeConfiguration()
}
} }
} }
@@ -113,9 +120,6 @@ class TunnelDetailTableViewController: NSViewController {
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
statusCheckbox.target = self
statusCheckbox.action = #selector(statusCheckboxToggled(sender:))
editButton.target = self editButton.target = self
editButton.action = #selector(handleEditTunnelAction) editButton.action = #selector(handleEditTunnelAction)
@@ -133,11 +137,9 @@ class TunnelDetailTableViewController: NSViewController {
containerView.addLayoutGuide(bottomControlsContainer) containerView.addLayoutGuide(bottomControlsContainer)
containerView.addSubview(box) containerView.addSubview(box)
containerView.addSubview(scrollView) containerView.addSubview(scrollView)
containerView.addSubview(statusCheckbox)
containerView.addSubview(editButton) containerView.addSubview(editButton)
box.translatesAutoresizingMaskIntoConstraints = false box.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
statusCheckbox.translatesAutoresizingMaskIntoConstraints = false
editButton.translatesAutoresizingMaskIntoConstraints = false editButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@@ -149,10 +151,8 @@ class TunnelDetailTableViewController: NSViewController {
bottomControlsContainer.heightAnchor.constraint(equalToConstant: 32), bottomControlsContainer.heightAnchor.constraint(equalToConstant: 32),
scrollView.bottomAnchor.constraint(equalTo: bottomControlsContainer.topAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomControlsContainer.topAnchor),
bottomControlsContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), bottomControlsContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
statusCheckbox.leadingAnchor.constraint(equalTo: bottomControlsContainer.leadingAnchor),
bottomControlsContainer.bottomAnchor.constraint(equalTo: statusCheckbox.bottomAnchor, constant: 4),
editButton.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor), editButton.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
bottomControlsContainer.bottomAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 4) bottomControlsContainer.bottomAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 0)
]) ])
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
@@ -162,6 +162,11 @@ class TunnelDetailTableViewController: NSViewController {
scrollView.trailingAnchor.constraint(equalTo: box.trailingAnchor) scrollView.trailingAnchor.constraint(equalTo: box.trailingAnchor)
]) ])
NSLayoutConstraint.activate([
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
])
view = containerView view = containerView
} }
@@ -170,7 +175,9 @@ class TunnelDetailTableViewController: NSViewController {
var interfaceSection = [(isVisible: Bool, modelRow: TableViewModelRow)]() var interfaceSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
for field in TunnelDetailTableViewController.interfaceFields { for field in TunnelDetailTableViewController.interfaceFields {
interfaceSection.append((isVisible: !tunnelViewModel.interfaceData[field].isEmpty, modelRow: .interfaceFieldRow(field))) let isStatus = field == .status || field == .toggleStatus
let isEmpty = tunnelViewModel.interfaceData[field].isEmpty
interfaceSection.append((isVisible: isStatus || !isEmpty, modelRow: .interfaceFieldRow(field)))
} }
interfaceSection.append((isVisible: true, modelRow: .spacerRow)) interfaceSection.append((isVisible: true, modelRow: .spacerRow))
modelRowsBySection.append(interfaceSection) modelRowsBySection.append(interfaceSection)
@@ -186,6 +193,9 @@ class TunnelDetailTableViewController: NSViewController {
var onDemandSection = [(isVisible: Bool, modelRow: TableViewModelRow)]() var onDemandSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
onDemandSection.append((isVisible: true, modelRow: .onDemandRow)) onDemandSection.append((isVisible: true, modelRow: .onDemandRow))
if onDemandViewModel.isWiFiInterfaceEnabled {
onDemandSection.append((isVisible: true, modelRow: .onDemandSSIDRow))
}
modelRowsBySection.append(onDemandSection) modelRowsBySection.append(onDemandSection)
tableViewModelRowsBySection = modelRowsBySection tableViewModelRowsBySection = modelRowsBySection
@@ -195,37 +205,6 @@ class TunnelDetailTableViewController: NSViewController {
tableViewModelRows = tableViewModelRowsBySection.flatMap { $0.filter { $0.isVisible }.map { $0.modelRow } } tableViewModelRows = tableViewModelRowsBySection.flatMap { $0.filter { $0.isVisible }.map { $0.modelRow } }
} }
func updateStatus() {
let statusText: String
switch tunnel.status {
case .waiting:
statusText = tr("tunnelStatusWaiting")
case .inactive:
statusText = tr("tunnelStatusInactive")
case .activating:
statusText = tr("tunnelStatusActivating")
case .active:
statusText = tr("tunnelStatusActive")
case .deactivating:
statusText = tr("tunnelStatusDeactivating")
case .reasserting:
statusText = tr("tunnelStatusReasserting")
case .restarting:
statusText = tr("tunnelStatusRestarting")
}
statusCheckbox.title = tr(format: "macStatus (%@)", statusText)
let shouldBeChecked = (tunnel.status != .inactive && tunnel.status != .deactivating)
let shouldBeEnabled = (tunnel.status == .active || tunnel.status == .inactive)
statusCheckbox.state = shouldBeChecked ? .on : .off
statusCheckbox.isEnabled = shouldBeEnabled
if tunnel.status == .active {
startUpdatingRuntimeConfiguration()
} else if tunnel.status == .inactive {
reloadRuntimeConfiguration()
stopUpdatingRuntimeConfiguration()
}
}
@objc func handleEditTunnelAction() { @objc func handleEditTunnelAction() {
PrivateDataConfirmation.confirmAccess(to: tr("macViewPrivateData")) { [weak self] in PrivateDataConfirmation.confirmAccess(to: tr("macViewPrivateData")) { [weak self] in
guard let self = self else { return } guard let self = self else { return }
@@ -244,12 +223,9 @@ class TunnelDetailTableViewController: NSViewController {
} }
} }
@objc func statusCheckboxToggled(sender: AnyObject?) { override func viewWillAppear() {
guard let statusCheckbox = sender as? NSButton else { return } if tunnel.status == .active {
if statusCheckbox.state == .on { startUpdatingRuntimeConfiguration()
tunnelsManager.startActivation(of: tunnel)
} else if statusCheckbox.state == .off {
tunnelsManager.startDeactivation(of: tunnel)
} }
} }
@@ -263,7 +239,24 @@ class TunnelDetailTableViewController: NSViewController {
func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) { func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
// Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering. // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
func sectionChanged<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], tableView: NSTableView, rowOffset: Int, changes: [T: TunnelViewModel.ChangeHandlers.FieldChange]) {
let tableView = self.tableView
func handleSectionFieldsModified<T>(fields: [T], modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
var modifiedRowIndices = IndexSet()
for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue }
if case .modified(_) = change {
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
modifiedRowIndices.insert(rowOffset + row)
}
}
if !modifiedRowIndices.isEmpty {
tableView.reloadData(forRowIndexes: modifiedRowIndices, columnIndexes: IndexSet(integer: 0))
}
}
func handleSectionFieldsAddedOrRemoved<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
for (index, field) in fields.enumerated() { for (index, field) in fields.enumerated() {
guard let change = changes[field] else { continue } guard let change = changes[field] else { continue }
let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
@@ -275,42 +268,54 @@ class TunnelDetailTableViewController: NSViewController {
tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade) tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
modelRowsInSection[index].isVisible = false modelRowsInSection[index].isVisible = false
case .modified: case .modified:
tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: []) break
tableView.insertRows(at: IndexSet(integer: rowOffset + row), withAnimation: [])
} }
} }
} }
var isChanged = false let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
let changeHandlers = TunnelViewModel.ChangeHandlers(
interfaceChanged: { [weak self] changes in if !changes.interfaceChanges.isEmpty {
guard let self = self else { return } handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields,
sectionChanged(fields: TunnelDetailTableViewController.interfaceFields, modelRowsInSection: &self.tableViewModelRowsBySection[0], modelRowsInSection: self.tableViewModelRowsBySection[0],
tableView: self.tableView, rowOffset: 0, changes: changes) rowOffset: 0, changes: changes.interfaceChanges)
isChanged = true }
}, for (peerIndex, peerChanges) in changes.peerChanges {
peerChangedAt: { [weak self] peerIndex, changes in let sectionIndex = 1 + peerIndex
guard let self = self else { return } let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
let sectionIndex = 1 + peerIndex handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields,
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count modelRowsInSection: self.tableViewModelRowsBySection[sectionIndex],
sectionChanged(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
tableView: self.tableView, rowOffset: rowOffset, changes: changes) }
isChanged = true
}, let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
peersRemovedAt: { [weak self] peerIndices in let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
guard let self = self else { return }
for peerIndex in peerIndices { if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !changes.peersRemovedIndices.isEmpty || !changes.peersInsertedIndices.isEmpty {
tableView.beginUpdates()
if isAnyInterfaceFieldAddedOrRemoved {
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields,
modelRowsInSection: &self.tableViewModelRowsBySection[0],
rowOffset: 0, changes: changes.interfaceChanges)
}
if isAnyPeerFieldAddedOrRemoved {
for (peerIndex, peerChanges) in changes.peerChanges {
let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
}
}
if !changes.peersRemovedIndices.isEmpty {
for peerIndex in changes.peersRemovedIndices {
let sectionIndex = 1 + peerIndex let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count
self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade) self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
self.tableViewModelRowsBySection.remove(at: sectionIndex) self.tableViewModelRowsBySection.remove(at: sectionIndex)
} }
isChanged = true }
}, if !changes.peersInsertedIndices.isEmpty {
peersInsertedAt: { [weak self] peerIndices in for peerIndex in changes.peersInsertedIndices {
guard let self = self else { return }
for peerIndex in peerIndices {
let peerData = self.tunnelViewModel.peersData[peerIndex] let peerData = self.tunnelViewModel.peersData[peerIndex]
let sectionIndex = 1 + peerIndex let sectionIndex = 1 + peerIndex
let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
@@ -322,16 +327,10 @@ class TunnelDetailTableViewController: NSViewController {
self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade) self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex) self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex)
} }
isChanged = true
} }
)
tableView.beginUpdates()
self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration, changeHandlers: changeHandlers)
if isChanged {
updateTableViewModelRows() updateTableViewModelRows()
tableView.endUpdates()
} }
tableView.endUpdates()
} }
private func reloadRuntimeConfiguration() { private func reloadRuntimeConfiguration() {
@@ -370,18 +369,26 @@ extension TunnelDetailTableViewController: NSTableViewDelegate {
let modelRow = tableViewModelRows[row] let modelRow = tableViewModelRows[row]
switch modelRow { switch modelRow {
case .interfaceFieldRow(let field): case .interfaceFieldRow(let field):
let cell: KeyValueRow = tableView.dequeueReusableCell() if field == .status {
let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString return statusCell()
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString) } else if field == .toggleStatus {
cell.value = tunnelViewModel.interfaceData[field] return toggleStatusCell()
cell.isKeyInBold = modelRow.isTitleRow() } else {
return cell let cell: KeyValueRow = tableView.dequeueReusableCell()
let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
cell.value = tunnelViewModel.interfaceData[field]
cell.isKeyInBold = modelRow.isTitleRow()
return cell
}
case .peerFieldRow(let peerData, let field): case .peerFieldRow(let peerData, let field):
let cell: KeyValueRow = tableView.dequeueReusableCell() let cell: KeyValueRow = tableView.dequeueReusableCell()
let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
cell.key = tr(format: "macFieldKey (%@)", localizedKeyString) cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
if field == .persistentKeepAlive { if field == .persistentKeepAlive {
cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field]) cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
} else if field == .preSharedKey {
cell.value = tr("tunnelPeerPresharedKeyEnabled")
} else { } else {
cell.value = peerData[field] cell.value = peerData[field]
} }
@@ -391,10 +398,103 @@ extension TunnelDetailTableViewController: NSTableViewDelegate {
return NSView() return NSView()
case .onDemandRow: case .onDemandRow:
let cell: KeyValueRow = tableView.dequeueReusableCell() let cell: KeyValueRow = tableView.dequeueReusableCell()
cell.key = tr("macFieldOnDemand") cell.key = modelRow.localizedSectionKeyString()
cell.value = TunnelViewModel.activateOnDemandDetailText(for: tunnel.activateOnDemandSetting) cell.value = onDemandViewModel.localizedInterfaceDescription
cell.isKeyInBold = true cell.isKeyInBold = true
return cell return cell
case .onDemandSSIDRow:
let cell: KeyValueRow = tableView.dequeueReusableCell()
cell.key = tr("macFieldOnDemandSSIDs")
let value: String
if onDemandViewModel.ssidOption == .anySSID {
value = onDemandViewModel.ssidOption.localizedUIString
} else {
value = tr(format: "tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)",
onDemandViewModel.ssidOption.localizedUIString,
onDemandViewModel.selectedSSIDs.joined(separator: ", "))
}
cell.value = value
cell.isKeyInBold = false
return cell
}
}
func statusCell() -> NSView {
let cell: KeyValueImageRow = tableView.dequeueReusableCell()
cell.key = tr(format: "macFieldKey (%@)", tr("tunnelInterfaceStatus"))
cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return }
cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
}
return cell
}
func toggleStatusCell() -> NSView {
let cell: ButtonRow = tableView.dequeueReusableCell()
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
cell.buttonToolTip = tr("macToolTipToggleStatus")
cell.onButtonClicked = { [weak self] in
self?.handleToggleActiveStatusAction()
}
cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
guard let cell = cell else { return }
cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
}
return cell
}
private static func localizedStatusDescription(forStatus status: TunnelStatus) -> String {
switch status {
case .inactive:
return tr("tunnelStatusInactive")
case .activating:
return tr("tunnelStatusActivating")
case .active:
return tr("tunnelStatusActive")
case .deactivating:
return tr("tunnelStatusDeactivating")
case .reasserting:
return tr("tunnelStatusReasserting")
case .restarting:
return tr("tunnelStatusRestarting")
case .waiting:
return tr("tunnelStatusWaiting")
}
}
private static func image(forStatus status: TunnelStatus?) -> NSImage? {
guard let status = status else { return nil }
switch status {
case .active, .restarting, .reasserting:
return NSImage(named: NSImage.statusAvailableName)
case .activating, .waiting, .deactivating:
return NSImage(named: NSImage.statusPartiallyAvailableName)
case .inactive:
return NSImage(named: NSImage.statusNoneName)
}
}
private static func localizedToggleStatusActionText(forStatus status: TunnelStatus) -> String {
switch status {
case .waiting:
return tr("macToggleStatusButtonWaiting")
case .inactive:
return tr("macToggleStatusButtonActivate")
case .activating:
return tr("macToggleStatusButtonActivating")
case .active:
return tr("macToggleStatusButtonDeactivate")
case .deactivating:
return tr("macToggleStatusButtonDeactivating")
case .reasserting:
return tr("macToggleStatusButtonReasserting")
case .restarting:
return tr("macToggleStatusButtonRestarting")
} }
} }
} }
@@ -402,9 +502,9 @@ extension TunnelDetailTableViewController: NSTableViewDelegate {
extension TunnelDetailTableViewController: TunnelEditViewControllerDelegate { extension TunnelDetailTableViewController: TunnelEditViewControllerDelegate {
func tunnelSaved(tunnel: TunnelContainer) { func tunnelSaved(tunnel: TunnelContainer) {
tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration) tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
updateTableViewModelRowsBySection() updateTableViewModelRowsBySection()
updateTableViewModelRows() updateTableViewModelRows()
updateStatus()
tableView.reloadData() tableView.reloadData()
self.tunnelEditVC = nil self.tunnelEditVC = nil
} }
@@ -42,11 +42,7 @@ class TunnelEditViewController: NSViewController {
return textView return textView
}() }()
let onDemandRow: PopupRow = { let onDemandControlsRow = OnDemandControlsRow()
let popupRow = PopupRow()
popupRow.key = tr("macFieldOnDemand")
return popupRow
}()
let scrollView: NSScrollView = { let scrollView: NSScrollView = {
let scrollView = NSScrollView() let scrollView = NSScrollView()
@@ -56,6 +52,14 @@ class TunnelEditViewController: NSViewController {
return scrollView return scrollView
}() }()
let excludePrivateIPsCheckbox: NSButton = {
let checkbox = NSButton()
checkbox.title = tr("tunnelPeerExcludePrivateIPs")
checkbox.setButtonType(.switch)
checkbox.state = .off
return checkbox
}()
let discardButton: NSButton = { let discardButton: NSButton = {
let button = NSButton() let button = NSButton()
button.title = tr("macEditDiscard") button.title = tr("macEditDiscard")
@@ -69,27 +73,27 @@ class TunnelEditViewController: NSViewController {
button.title = tr("macEditSave") button.title = tr("macEditSave")
button.setButtonType(.momentaryPushIn) button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded button.bezelStyle = .rounded
button.keyEquivalent = "s"
button.keyEquivalentModifierMask = [.command]
return button return button
}() }()
let activateOnDemandOptions: [ActivateOnDemandOption] = [
.none,
.useOnDemandOverWiFiOrEthernet,
.useOnDemandOverWiFiOnly,
.useOnDemandOverEthernetOnly
]
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
let tunnel: TunnelContainer? let tunnel: TunnelContainer?
var onDemandViewModel: ActivateOnDemandViewModel
weak var delegate: TunnelEditViewControllerDelegate? weak var delegate: TunnelEditViewControllerDelegate?
var privateKeyObservationToken: AnyObject? var privateKeyObservationToken: AnyObject?
var hasErrorObservationToken: AnyObject? var hasErrorObservationToken: AnyObject?
var singlePeerAllowedIPsObservationToken: AnyObject?
var dnsServersAddedToAllowedIPs: String?
init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) { init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) {
self.tunnelsManager = tunnelsManager self.tunnelsManager = tunnelsManager
self.tunnel = tunnel self.tunnel = tunnel
self.onDemandViewModel = tunnel != nil ? ActivateOnDemandViewModel(tunnel: tunnel!) : ActivateOnDemandViewModel()
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
@@ -97,8 +101,7 @@ class TunnelEditViewController: NSViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
func populateTextFields() { func populateFields() {
let selectedActivateOnDemandOption: ActivateOnDemandOption
if let tunnel = tunnel { if let tunnel = tunnel {
// Editing an existing tunnel // Editing an existing tunnel
let tunnelConfiguration = tunnel.tunnelConfiguration! let tunnelConfiguration = tunnel.tunnelConfiguration!
@@ -106,11 +109,9 @@ class TunnelEditViewController: NSViewController {
textView.string = tunnelConfiguration.asWgQuickConfig() textView.string = tunnelConfiguration.asWgQuickConfig()
publicKeyRow.value = tunnelConfiguration.interface.publicKey.base64Key() ?? "" publicKeyRow.value = tunnelConfiguration.interface.publicKey.base64Key() ?? ""
textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64Key() ?? "" textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64Key() ?? ""
if tunnel.activateOnDemandSetting.isActivateOnDemandEnabled { let singlePeer = tunnelConfiguration.peers.count == 1 ? tunnelConfiguration.peers.first : nil
selectedActivateOnDemandOption = tunnel.activateOnDemandSetting.activateOnDemandOption updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: singlePeer?.allowedIPs.map { $0.stringRepresentation })
} else { dnsServersAddedToAllowedIPs = excludePrivateIPsCheckbox.state == .on ? tunnelConfiguration.interface.dns.map { $0.stringRepresentation }.joined(separator: ", ") : nil
selectedActivateOnDemandOption = .none
}
} else { } else {
// Creating a new tunnel // Creating a new tunnel
let privateKey = Curve25519.generatePrivateKey() let privateKey = Curve25519.generatePrivateKey()
@@ -118,7 +119,8 @@ class TunnelEditViewController: NSViewController {
let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key() ?? "")\n" let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key() ?? "")\n"
publicKeyRow.value = publicKey.base64Key() ?? "" publicKeyRow.value = publicKey.base64Key() ?? ""
textView.string = bootstrappingText textView.string = bootstrappingText
selectedActivateOnDemandOption = .none updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: nil)
dnsServersAddedToAllowedIPs = nil
} }
privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in
if let privateKeyString = textView.privateKeyString, if let privateKeyString = textView.privateKeyString,
@@ -133,13 +135,13 @@ class TunnelEditViewController: NSViewController {
hasErrorObservationToken = textView.observe(\.hasError) { [weak saveButton] textView, _ in hasErrorObservationToken = textView.observe(\.hasError) { [weak saveButton] textView, _ in
saveButton?.isEnabled = !textView.hasError saveButton?.isEnabled = !textView.hasError
} }
singlePeerAllowedIPsObservationToken = textView.observe(\.singlePeerAllowedIPs) { [weak self] textView, _ in
onDemandRow.valueOptions = activateOnDemandOptions.map { TunnelViewModel.activateOnDemandOptionText(for: $0) } self?.updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: textView.singlePeerAllowedIPs)
onDemandRow.selectedOptionIndex = activateOnDemandOptions.firstIndex(of: selectedActivateOnDemandOption)! }
} }
override func loadView() { override func loadView() {
populateTextFields() populateFields()
scrollView.documentView = textView scrollView.documentView = textView
@@ -149,16 +151,22 @@ class TunnelEditViewController: NSViewController {
discardButton.target = self discardButton.target = self
discardButton.action = #selector(handleDiscardAction) discardButton.action = #selector(handleDiscardAction)
excludePrivateIPsCheckbox.target = self
excludePrivateIPsCheckbox.action = #selector(excludePrivateIPsCheckboxToggled(sender:))
onDemandControlsRow.onDemandViewModel = onDemandViewModel
let margin: CGFloat = 20 let margin: CGFloat = 20
let internalSpacing: CGFloat = 10 let internalSpacing: CGFloat = 10
let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandRow, scrollView]) let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandControlsRow, scrollView])
editorStackView.orientation = .vertical editorStackView.orientation = .vertical
editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal) editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal)
editorStackView.spacing = internalSpacing editorStackView.spacing = internalSpacing
let buttonRowStackView = NSStackView() let buttonRowStackView = NSStackView()
buttonRowStackView.setViews([discardButton, saveButton], in: .trailing) buttonRowStackView.setViews([discardButton, saveButton], in: .trailing)
buttonRowStackView.addView(excludePrivateIPsCheckbox, in: .leading)
buttonRowStackView.orientation = .horizontal buttonRowStackView.orientation = .horizontal
buttonRowStackView.spacing = internalSpacing buttonRowStackView.spacing = internalSpacing
@@ -177,19 +185,22 @@ class TunnelEditViewController: NSViewController {
self.view = containerView self.view = containerView
} }
func setUserInteractionEnabled(_ enabled: Bool) {
view.window?.ignoresMouseEvents = !enabled
nameRow.valueLabel.isEditable = enabled
textView.isEditable = enabled
onDemandControlsRow.onDemandSSIDsField.isEnabled = enabled
}
@objc func handleSaveAction() { @objc func handleSaveAction() {
let name = nameRow.value let name = nameRow.value
guard !name.isEmpty else { guard !name.isEmpty else {
ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self) ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self)
return return
} }
let onDemandSetting: ActivateOnDemandSetting
let onDemandOption = activateOnDemandOptions[onDemandRow.selectedOptionIndex] onDemandControlsRow.saveToViewModel()
if onDemandOption == .none { let onDemandOption = onDemandViewModel.toOnDemandOption()
onDemandSetting = ActivateOnDemandSetting.defaultSetting
} else {
onDemandSetting = ActivateOnDemandSetting(isActivateOnDemandEnabled: true, activateOnDemandOption: onDemandOption)
}
let isTunnelModifiedWithoutChangingName = (tunnel != nil && tunnel!.name == name) let isTunnelModifiedWithoutChangingName = (tunnel != nil && tunnel!.name == name)
guard isTunnelModifiedWithoutChangingName || tunnelsManager.tunnel(named: name) == nil else { guard isTunnelModifiedWithoutChangingName || tunnelsManager.tunnel(named: name) == nil else {
@@ -197,7 +208,7 @@ class TunnelEditViewController: NSViewController {
return return
} }
let tunnelConfiguration: TunnelConfiguration var tunnelConfiguration: TunnelConfiguration
do { do {
tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value)
} catch let error as WireGuardAppError { } catch let error as WireGuardAppError {
@@ -207,32 +218,82 @@ class TunnelEditViewController: NSViewController {
fatalError() fatalError()
} }
if excludePrivateIPsCheckbox.state == .on, tunnelConfiguration.peers.count == 1, let dnsServersAddedToAllowedIPs = dnsServersAddedToAllowedIPs {
// Update the DNS servers in the AllowedIPs
let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
let originalAllowedIPs = tunnelViewModel.peersData[0][.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
let dnsServersInAllowedIPs = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(dnsServersAddedToAllowedIPs.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
let dnsServersCurrent = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(tunnelViewModel.interfaceData[.dns].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
let modifiedAllowedIPs = originalAllowedIPs.filter { !dnsServersInAllowedIPs.contains($0) } + dnsServersCurrent
tunnelViewModel.peersData[0][.allowedIPs] = modifiedAllowedIPs.joined(separator: ", ")
let saveResult = tunnelViewModel.save()
if case .saved(let modifiedTunnelConfiguration) = saveResult {
tunnelConfiguration = modifiedTunnelConfiguration
}
}
setUserInteractionEnabled(false)
if let tunnel = tunnel { if let tunnel = tunnel {
// We're modifying an existing tunnel // We're modifying an existing tunnel
tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] error in tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
guard let self = self else { return }
self.setUserInteractionEnabled(true)
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
return return
} }
self?.dismiss(self) self.delegate?.tunnelSaved(tunnel: tunnel)
self?.delegate?.tunnelSaved(tunnel: tunnel) self.presentingViewController?.dismiss(self)
} }
} else { } else {
// We're creating a new tunnel // We're creating a new tunnel
tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, activateOnDemandSetting: onDemandSetting) { [weak self] result in self.tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
if let error = result.error { guard let self = self else { return }
self.setUserInteractionEnabled(true)
switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
return case .success(let tunnel):
self.delegate?.tunnelSaved(tunnel: tunnel)
self.presentingViewController?.dismiss(self)
} }
let tunnel: TunnelContainer = result.value!
self?.dismiss(self)
self?.delegate?.tunnelSaved(tunnel: tunnel)
} }
} }
} }
@objc func handleDiscardAction() { @objc func handleDiscardAction() {
delegate?.tunnelEditingCancelled() delegate?.tunnelEditingCancelled()
dismiss(self) presentingViewController?.dismiss(self)
}
func updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: [String]?) {
let shouldAllowExcludePrivateIPsControl: Bool
let excludePrivateIPsValue: Bool
if let singlePeerAllowedIPs = singlePeerAllowedIPs {
(shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: true, allowedIPs: Set<String>(singlePeerAllowedIPs))
} else {
(shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: false, allowedIPs: Set<String>())
}
excludePrivateIPsCheckbox.isHidden = !shouldAllowExcludePrivateIPsControl
excludePrivateIPsCheckbox.state = excludePrivateIPsValue ? .on : .off
}
@objc func excludePrivateIPsCheckboxToggled(sender: AnyObject?) {
guard let excludePrivateIPsCheckbox = sender as? NSButton else { return }
guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) else { return }
let isOn = excludePrivateIPsCheckbox.state == .on
let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
tunnelViewModel.peersData.first?.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: tunnelViewModel.interfaceData[.dns], oldDNSServers: dnsServersAddedToAllowedIPs)
if let modifiedConfig = tunnelViewModel.asWgQuickConfig() {
textView.setConfText(modifiedConfig)
dnsServersAddedToAllowedIPs = isOn ? tunnelViewModel.interfaceData[.dns] : nil
}
}
}
extension TunnelEditViewController {
override func cancelOperation(_ sender: Any?) {
handleDiscardAction()
} }
} }
@@ -4,7 +4,7 @@
import Cocoa import Cocoa
protocol TunnelsListTableViewControllerDelegate: class { protocol TunnelsListTableViewControllerDelegate: class {
func tunnelSelected(tunnel: TunnelContainer) func tunnelsSelected(tunnelIndices: [Int])
func tunnelsListEmpty() func tunnelsListEmpty()
} }
@@ -12,41 +12,57 @@ class TunnelsListTableViewController: NSViewController {
let tunnelsManager: TunnelsManager let tunnelsManager: TunnelsManager
weak var delegate: TunnelsListTableViewControllerDelegate? weak var delegate: TunnelsListTableViewControllerDelegate?
var isRemovingTunnelsFromWithinTheApp = false
let tableView: NSTableView = { let tableView: NSTableView = {
let tableView = NSTableView() let tableView = NSTableView()
tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelsList"))) tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelsList")))
tableView.headerView = nil tableView.headerView = nil
tableView.rowSizeStyle = .medium tableView.rowSizeStyle = .medium
tableView.allowsMultipleSelection = true
return tableView return tableView
}() }()
let buttonBar: NSSegmentedControl = { let addButton: NSPopUpButton = {
let addButtonImage = NSImage(named: NSImage.addTemplateName)! let imageItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
let removeButtonImage = NSImage(named: NSImage.removeTemplateName)! imageItem.image = NSImage(named: NSImage.addTemplateName)!
let actionButtonImage = NSImage(named: NSImage.actionTemplateName)!
let buttonBar = NSSegmentedControl(images: [addButtonImage, removeButtonImage, actionButtonImage], let menu = NSMenu()
trackingMode: .momentary, target: nil, action: #selector(buttonBarClicked(sender:))) menu.addItem(imageItem)
buttonBar.segmentStyle = .smallSquare menu.addItem(withTitle: tr("macMenuAddEmptyTunnel"), action: #selector(handleAddEmptyTunnelAction), keyEquivalent: "n")
buttonBar.segmentDistribution = .fit menu.addItem(withTitle: tr("macMenuImportTunnels"), action: #selector(handleImportTunnelAction), keyEquivalent: "o")
buttonBar.setShowsMenuIndicator(true, forSegment: 0) menu.autoenablesItems = false
buttonBar.setShowsMenuIndicator(false, forSegment: 1)
buttonBar.setShowsMenuIndicator(true, forSegment: 2) let button = NSPopUpButton(frame: NSRect.zero, pullsDown: true)
return buttonBar button.menu = menu
button.bezelStyle = .smallSquare
(button.cell as? NSPopUpButtonCell)?.arrowPosition = .arrowAtBottom
return button
}() }()
let addMenu: NSMenu = { let removeButton: NSButton = {
let addMenu = NSMenu(title: "TunnelsListAdd") let image = NSImage(named: NSImage.removeTemplateName)!
addMenu.addItem(withTitle: tr("macMenuAddEmptyTunnel"), action: #selector(handleAddEmptyTunnelAction), keyEquivalent: "n") let button = NSButton(image: image, target: self, action: #selector(handleRemoveTunnelAction))
addMenu.addItem(withTitle: tr("macMenuImportTunnels"), action: #selector(handleImportTunnelAction), keyEquivalent: "i") button.bezelStyle = .smallSquare
return addMenu button.imagePosition = .imageOnly
return button
}() }()
let actionMenu: NSMenu = { let actionButton: NSPopUpButton = {
let actionMenu = NSMenu(title: "TunnelsListAction") let imageItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
actionMenu.addItem(withTitle: tr("macMenuExportLog"), action: #selector(handleExportLogAction), keyEquivalent: "") imageItem.image = NSImage(named: NSImage.actionTemplateName)!
actionMenu.addItem(withTitle: tr("macMenuExportTunnels"), action: #selector(handleExportTunnelsAction), keyEquivalent: "")
return actionMenu let menu = NSMenu()
menu.addItem(imageItem)
menu.addItem(withTitle: tr("macMenuViewLog"), action: #selector(handleViewLogAction), keyEquivalent: "")
menu.addItem(withTitle: tr("macMenuExportTunnels"), action: #selector(handleExportTunnelsAction), keyEquivalent: "")
menu.autoenablesItems = false
let button = NSPopUpButton(frame: NSRect.zero, pullsDown: true)
button.menu = menu
button.bezelStyle = .smallSquare
(button.cell as? NSPopUpButtonCell)?.arrowPosition = .arrowAtBottom
return button
}() }()
init(tunnelsManager: TunnelsManager) { init(tunnelsManager: TunnelsManager) {
@@ -62,10 +78,13 @@ class TunnelsListTableViewController: NSViewController {
tableView.dataSource = self tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
tableView.doubleAction = #selector(listDoubleClicked(sender:))
let isSelected = selectTunnelInOperation() || selectTunnel(at: 0) let isSelected = selectTunnelInOperation() || selectTunnel(at: 0)
if !isSelected { if !isSelected {
delegate?.tunnelsListEmpty() delegate?.tunnelsListEmpty()
} }
tableView.allowsEmptySelection = false
let scrollView = NSScrollView() let scrollView = NSScrollView()
scrollView.hasVerticalScroller = true scrollView.hasVerticalScroller = true
@@ -76,6 +95,16 @@ class TunnelsListTableViewController: NSViewController {
clipView.documentView = tableView clipView.documentView = tableView
scrollView.contentView = clipView scrollView.contentView = clipView
let buttonBar = NSStackView(views: [addButton, removeButton, actionButton])
buttonBar.orientation = .horizontal
buttonBar.spacing = -1
NSLayoutConstraint.activate([
removeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 26),
removeButton.topAnchor.constraint(equalTo: buttonBar.topAnchor),
removeButton.bottomAnchor.constraint(equalTo: buttonBar.bottomAnchor)
])
let fillerButton = FillerButton() let fillerButton = FillerButton()
let containerView = NSView() let containerView = NSView()
@@ -100,13 +129,12 @@ class TunnelsListTableViewController: NSViewController {
]) ])
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 120), containerView.widthAnchor.constraint(equalToConstant: 180),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120) containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
]) ])
buttonBar.target = self addButton.menu?.items.forEach { $0.target = self }
addMenu.items.forEach { $0.target = self } actionButton.menu?.items.forEach { $0.target = self }
actionMenu.items.forEach { $0.target = self }
view = containerView view = containerView
} }
@@ -123,22 +151,6 @@ class TunnelsListTableViewController: NSViewController {
return false return false
} }
@objc func buttonBarClicked(sender: AnyObject?) {
guard let buttonBar = sender as? NSSegmentedControl else { return }
// We have to resort to explicitly showing the menu instead of using NSSegmentedControl.setMenu()
// because we have a mix of menu and non-menu segments.
// See: http://openradar.appspot.com/radar?id=61419
if buttonBar.selectedSegment == 0 {
let segmentBottomLeft = NSPoint(x: 0, y: buttonBar.bounds.height + 2)
addMenu.popUp(positioning: nil, at: segmentBottomLeft, in: buttonBar)
} else if buttonBar.selectedSegment == 1 {
handleRemoveTunnelAction()
} else if buttonBar.selectedSegment == 2 {
let segmentBottomLeft = NSPoint(x: buttonBar.bounds.width * 0.66, y: buttonBar.bounds.height + 2)
actionMenu.popUp(positioning: nil, at: segmentBottomLeft, in: buttonBar)
}
}
@objc func handleAddEmptyTunnelAction() { @objc func handleAddEmptyTunnelAction() {
let tunnelEditVC = TunnelEditViewController(tunnelsManager: tunnelsManager, tunnel: nil) let tunnelEditVC = TunnelEditViewController(tunnelsManager: tunnelsManager, tunnel: nil)
tunnelEditVC.delegate = self tunnelEditVC.delegate = self
@@ -151,59 +163,43 @@ class TunnelsListTableViewController: NSViewController {
@objc func handleRemoveTunnelAction() { @objc func handleRemoveTunnelAction() {
guard let window = view.window else { return } guard let window = view.window else { return }
let selectedTunnelIndex = tableView.selectedRow
guard selectedTunnelIndex >= 0 && selectedTunnelIndex < tunnelsManager.numberOfTunnels() else { return } let selectedTunnelIndices = tableView.selectedRowIndexes.sorted().filter { $0 >= 0 && $0 < tunnelsManager.numberOfTunnels() }
let selectedTunnel = tunnelsManager.tunnel(at: selectedTunnelIndex) guard !selectedTunnelIndices.isEmpty else { return }
let alert = NSAlert() var nextSelection = selectedTunnelIndices.last! + 1
alert.messageText = tr(format: "macDeleteTunnelConfirmationAlertMessage (%@)", selectedTunnel.name) if nextSelection >= tunnelsManager.numberOfTunnels() {
nextSelection = max(selectedTunnelIndices.first! - 1, 0)
}
let alert = DeleteTunnelsConfirmationAlert()
if selectedTunnelIndices.count == 1 {
let firstSelectedTunnel = tunnelsManager.tunnel(at: selectedTunnelIndices.first!)
alert.messageText = tr(format: "macDeleteTunnelConfirmationAlertMessage (%@)", firstSelectedTunnel.name)
} else {
alert.messageText = tr(format: "macDeleteMultipleTunnelsConfirmationAlertMessage (%d)", selectedTunnelIndices.count)
}
alert.informativeText = tr("macDeleteTunnelConfirmationAlertInfo") alert.informativeText = tr("macDeleteTunnelConfirmationAlertInfo")
alert.addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleDelete")) alert.onDeleteClicked = { [weak self] completion in
alert.addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleCancel")) guard let self = self else { return }
alert.beginSheetModal(for: window) { [weak self] response in self.selectTunnel(at: nextSelection)
guard response == .alertFirstButtonReturn else { return } let selectedTunnels = selectedTunnelIndices.map { self.tunnelsManager.tunnel(at: $0) }
self?.buttonBar.setEnabled(false, forSegment: 1) self.isRemovingTunnelsFromWithinTheApp = true
self?.tunnelsManager.remove(tunnel: selectedTunnel) { [weak self] error in self.tunnelsManager.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
guard let self = self else { return } guard let self = self else { return }
defer { self.buttonBar.setEnabled(true, forSegment: 1) } self.isRemovingTunnelsFromWithinTheApp = false
defer { completion() }
if let error = error { if let error = error {
ErrorPresenter.showErrorAlert(error: error, from: self) ErrorPresenter.showErrorAlert(error: error, from: self)
return return
} }
} }
} }
alert.beginSheetModal(for: window)
} }
@objc func handleExportLogAction() { @objc func handleViewLogAction() {
guard let window = view.window else { return } let logVC = LogViewController()
let savePanel = NSSavePanel() self.presentAsSheet(logVC)
savePanel.prompt = tr("macSheetButtonExportLog")
savePanel.nameFieldLabel = tr("macNameFieldExportLog")
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
let timeStampString = dateFormatter.string(from: Date())
savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
guard let networkExtensionLogFilePath = FileManager.networkExtensionLogFileURL?.path else {
ErrorPresenter.showErrorAlert(title: tr("alertUnableToFindExtensionLogPathTitle"), message: tr("alertUnableToFindExtensionLogPathMessage"), from: self)
return
}
savePanel.beginSheetModal(for: window) { response in
guard response == .OK else { return }
guard let destinationURL = savePanel.url else { return }
DispatchQueue.global(qos: .userInitiated).async {
let isWritten = Logger.global?.writeLog(called: "APP", mergedWith: networkExtensionLogFilePath, called: "NET", to: destinationURL.path) ?? false
guard isWritten else {
DispatchQueue.main.async { [weak self] in
ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
}
return
}
}
}
} }
@objc func handleExportTunnelsAction() { @objc func handleExportTunnelsAction() {
@@ -232,9 +228,21 @@ class TunnelsListTableViewController: NSViewController {
} }
} }
@objc func listDoubleClicked(sender: AnyObject) {
let tunnelIndex = tableView.clickedRow
guard tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() else { return }
let tunnel = tunnelsManager.tunnel(at: tunnelIndex)
if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
}
}
@discardableResult @discardableResult
private func selectTunnel(at index: Int) -> Bool { private func selectTunnel(at index: Int) -> Bool {
if index < tunnelsManager.numberOfTunnels() { if index < tunnelsManager.numberOfTunnels() {
tableView.scrollRowToVisible(index)
tableView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false) tableView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false)
return true return true
} }
@@ -275,14 +283,14 @@ extension TunnelsListTableViewController {
} }
func tunnelRemoved(at index: Int) { func tunnelRemoved(at index: Int) {
let selectedTunnelIndex = tableView.selectedRow let selectedIndices = tableView.selectedRowIndexes
let isSingleSelectedTunnelBeingRemoved = selectedIndices.contains(index) && selectedIndices.count == 1
tableView.removeRows(at: IndexSet(integer: index), withAnimation: .slideLeft) tableView.removeRows(at: IndexSet(integer: index), withAnimation: .slideLeft)
if tunnelsManager.numberOfTunnels() == 0 { if tunnelsManager.numberOfTunnels() == 0 {
delegate?.tunnelsListEmpty() delegate?.tunnelsListEmpty()
} } else if !isRemovingTunnelsFromWithinTheApp && isSingleSelectedTunnelBeingRemoved {
let tunnelIndex = min(selectedTunnelIndex, self.tunnelsManager.numberOfTunnels() - 1) let newSelection = min(index, tunnelsManager.numberOfTunnels() - 1)
if tunnelIndex >= 0 { tableView.selectRowIndexes(IndexSet(integer: newSelection), byExtendingSelection: false)
self.selectTunnel(at: tunnelIndex)
} }
} }
} }
@@ -301,9 +309,27 @@ extension TunnelsListTableViewController: NSTableViewDelegate {
} }
func tableViewSelectionDidChange(_ notification: Notification) { func tableViewSelectionDidChange(_ notification: Notification) {
guard tableView.selectedRow >= 0 else { return } let selectedTunnelIndices = tableView.selectedRowIndexes.sorted()
let selectedTunnel = tunnelsManager.tunnel(at: tableView.selectedRow) if !selectedTunnelIndices.isEmpty {
delegate?.tunnelSelected(tunnel: selectedTunnel) delegate?.tunnelsSelected(tunnelIndices: tableView.selectedRowIndexes.sorted())
}
}
}
extension TunnelsListTableViewController {
override func keyDown(with event: NSEvent) {
if event.specialKey == .delete {
handleRemoveTunnelAction()
}
}
}
extension TunnelsListTableViewController: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(TunnelsListTableViewController.handleRemoveTunnelAction) {
return !tableView.selectedRowIndexes.isEmpty
}
return true
} }
} }
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
import Cocoa
class UnusableTunnelDetailViewController: NSViewController {
var onButtonClicked: (() -> Void)?
let messageLabel: NSTextField = {
let text = tr("macUnusableTunnelMessage")
let boldFont = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize)
let boldText = NSAttributedString(string: text, attributes: [.font: boldFont])
let label = NSTextField(labelWithAttributedString: boldText)
return label
}()
let infoLabel: NSTextField = {
let label = NSTextField(wrappingLabelWithString: tr("macUnusableTunnelInfo"))
return label
}()
let button: NSButton = {
let button = NSButton()
button.title = tr("macUnusableTunnelButtonTitleDeleteTunnel")
button.setButtonType(.momentaryPushIn)
button.bezelStyle = .rounded
return button
}()
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
button.target = self
button.action = #selector(buttonClicked)
let margin: CGFloat = 20
let internalSpacing: CGFloat = 20
let buttonSpacing: CGFloat = 30
let stackView = NSStackView(views: [messageLabel, infoLabel, button])
stackView.orientation = .vertical
stackView.edgeInsets = NSEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
stackView.spacing = internalSpacing
stackView.setCustomSpacing(buttonSpacing, after: infoLabel)
let view = NSView()
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.widthAnchor.constraint(equalToConstant: 360),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
view.widthAnchor.constraint(greaterThanOrEqualToConstant: 420),
view.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
])
self.view = view
}
@objc func buttonClicked() {
onButtonClicked?()
}
}
@@ -5,3 +5,8 @@
#include "ringlogger.h" #include "ringlogger.h"
#include "highlighter.h" #include "highlighter.h"
#include "key.h" #include "key.h"
#import "TargetConditionals.h"
#if TARGET_OS_OSX
#include <libproc.h>
#endif
@@ -7,6 +7,7 @@ enum ZipArchiveError: WireGuardAppError {
case cantOpenInputZipFile case cantOpenInputZipFile
case cantOpenOutputZipFileForWriting case cantOpenOutputZipFileForWriting
case badArchive case badArchive
case noTunnelsInZipArchive
var alertText: AlertText { var alertText: AlertText {
switch self { switch self {
@@ -16,6 +17,8 @@ enum ZipArchiveError: WireGuardAppError {
return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage")) return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
case .badArchive: case .badArchive:
return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage")) return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
case .noTunnelsInZipArchive:
return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
} }
} }
} }
@@ -31,7 +34,7 @@ class ZipArchive {
let fileName = input.fileName let fileName = input.fileName
let contents = input.contents let contents = input.contents
zipOpenNewFileInZip(zipFile, fileName.cString(using: .utf8), nil, nil, 0, nil, 0, nil, Z_DEFLATED, Z_DEFAULT_COMPRESSION) zipOpenNewFileInZip(zipFile, fileName.cString(using: .utf8), nil, nil, 0, nil, 0, nil, Z_DEFLATED, Z_DEFAULT_COMPRESSION)
contents.withUnsafeBytes { (ptr: UnsafePointer<UInt8>) -> Void in contents.withUnsafeUInt8Bytes { ptr -> Void in
zipWriteInFileInZip(zipFile, UnsafeRawPointer(ptr), UInt32(contents.count)) zipWriteInFileInZip(zipFile, UnsafeRawPointer(ptr), UInt32(contents.count))
} }
zipCloseFileInZip(zipFile) zipCloseFileInZip(zipFile)
@@ -42,6 +45,7 @@ class ZipArchive {
static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] { static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
var results = [(fileBaseName: String, contents: Data)]() var results = [(fileBaseName: String, contents: Data)]()
var requiredFileExtensionsLowercased = requiredFileExtensions.map { $0.lowercased() }
guard let zipFile = unzOpen64(url.path) else { guard let zipFile = unzOpen64(url.path) else {
throw ZipArchiveError.cantOpenInputZipFile throw ZipArchiveError.cantOpenInputZipFile
@@ -70,7 +74,7 @@ class ZipArchive {
let isDirectory = (lastChar == "/" || lastChar == "\\") let isDirectory = (lastChar == "/" || lastChar == "\\")
let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil) let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
if !isDirectory && requiredFileExtensions.contains(fileURL.pathExtension) { if !isDirectory && requiredFileExtensionsLowercased.contains(fileURL.pathExtension.lowercased()) {
var unzippedData = Data() var unzippedData = Data()
var bytesRead: Int32 = 0 var bytesRead: Int32 = 0
repeat { repeat {
@@ -3,16 +3,8 @@
import Foundation import Foundation
enum ZipImporterError: WireGuardAppError {
case noTunnelsInZipArchive
var alertText: AlertText {
return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
}
}
class ZipImporter { class ZipImporter {
static func importConfigFiles(from url: URL, completion: @escaping (WireGuardResult<[TunnelConfiguration?]>) -> Void) { static func importConfigFiles(from url: URL, completion: @escaping (Result<[TunnelConfiguration?], ZipArchiveError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
var unarchivedFiles: [(fileBaseName: String, contents: Data)] var unarchivedFiles: [(fileBaseName: String, contents: Data)]
do { do {
@@ -28,16 +20,16 @@ class ZipImporter {
} }
if unarchivedFiles.isEmpty { if unarchivedFiles.isEmpty {
throw ZipImporterError.noTunnelsInZipArchive throw ZipArchiveError.noTunnelsInZipArchive
} }
} catch let error as WireGuardAppError { } catch let error as ZipArchiveError {
DispatchQueue.main.async { completion(.failure(error)) } DispatchQueue.main.async { completion(.failure(error)) }
return return
} catch { } catch {
fatalError() fatalError()
} }
unarchivedFiles.sort { $0.fileBaseName < $1.fileBaseName } unarchivedFiles.sort { TunnelsManager.tunnelNameIsLessThan($0.fileBaseName, $1.fileBaseName) }
var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count) var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
for (index, file) in unarchivedFiles.enumerated() { for (index, file) in unarchivedFiles.enumerated() {
if index > 0 && file == unarchivedFiles[index - 1] { if index > 0 && file == unarchivedFiles[index - 1] {
@@ -117,6 +117,8 @@ extension Endpoint {
hostname = "\(address)" hostname = "\(address)"
case .ipv6(let address): case .ipv6(let address):
hostname = "\(address)" hostname = "\(address)"
@unknown default:
fatalError()
} }
var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0)) var resultPointer = UnsafeMutablePointer<addrinfo>(OpaquePointer(bitPattern: 0))
@@ -99,7 +99,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107). // HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to // Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users. // sufficient quantities of users.
exit(0) let osVersion = ProcessInfo.processInfo.operatingSystemVersion
if osVersion.majorVersion <= 10 && osVersion.minorVersion <= 14 {
exit(0)
}
#endif #endif
} }
@@ -122,7 +125,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
} }
private func configureLogger() { private func configureLogger() {
Logger.configureGlobal(withFilePath: FileManager.networkExtensionLogFileURL?.path) Logger.configureGlobal(tagged: "NET", withFilePath: FileManager.logFileURL?.path)
wgSetLogger { level, msgC in wgSetLogger { level, msgC in
guard let msgC = msgC else { return } guard let msgC = msgC else { return }
let logType: OSLogType let logType: OSLogType
@@ -143,18 +146,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func pathUpdate(path: Network.NWPath) { private func pathUpdate(path: Network.NWPath) {
guard let handle = handle else { return } guard let handle = handle else { return }
wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)") wg_log(.debug, message: "Network change detected with \(path.status) route and interface order \(path.availableInterfaces)")
#if os(iOS) #if os(iOS)
if let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator { if let packetTunnelSettingsGenerator = packetTunnelSettingsGenerator {
_ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) } _ = packetTunnelSettingsGenerator.endpointUapiConfiguration().withGoString { return wgSetConfig(handle, $0) }
} }
#endif #endif
var interfaces = path.availableInterfaces wgBumpSockets(handle)
if let ifname = ifname {
interfaces = interfaces.filter { $0.name != ifname }
}
if let ifscope = interfaces.first?.index {
wgBindInterfaceScope(handle, Int32(ifscope))
}
} }
} }
Submodule wireguard-go deleted from c4b43e35a7
+16 -55
View File
@@ -14,73 +14,37 @@ LIPO ?= lipo
DESTDIR ?= $(CONFIGURATION_BUILD_DIR) DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge
UPSTREAM_FILES := $(filter-out %/main.go %/queueconstants.go,$(wildcard ../wireguard-go/*/*.go) $(wildcard ../wireguard-go/*.go)) ../wireguard-go/go.mod ../wireguard-go/go.sum
DOWNSTREAM_FILES := $(wildcard src/*.go) $(wildcard src/*/*.go)
CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -Wno-unused-command-line-argument -isysroot $(SDKROOT) -arch CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -Wno-unused-command-line-argument -isysroot $(SDKROOT) -arch
GOARCH_arm64 := arm64 GOARCH_arm64 := arm64
GOARCH_armv7 := arm GOARCH_armv7 := arm
GOARCH_x86_64 := amd64 GOARCH_x86_64 := amd64
export GOOS := darwin
export CGO_ENABLED := 1
build: $(DESTDIR)/libwg-go.a build: $(DESTDIR)/libwg-go.a
version-header: $(DESTDIR)/wireguard-go-version.h version-header: $(DESTDIR)/wireguard-go-version.h
GOBUILDARCH := $(GOARCH_$(shell uname -m)) REAL_GOROOT := $(shell go env GOROOT 2>/dev/null)
GOBUILDOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
GOBUILDVERSION := 1.11.5
GOBUILDTARBALL := go$(GOBUILDVERSION).$(GOBUILDOS)-$(GOBUILDARCH).tar.gz
GOBUILDTARBALLURL := https://dl.google.com/go/$(GOBUILDTARBALL)
GOBUILDVERSION_NEEDED := go version go$(GOBUILDVERSION) $(GOBUILDOS)/$(GOBUILDARCH)
export GOROOT := $(BUILDDIR)/goroot export GOROOT := $(BUILDDIR)/goroot
export GOPATH := $(BUILDDIR)/gopath $(GOROOT)/.prepared:
export PATH := $(GOROOT)/bin:$(PATH) [ -n "$(REAL_GOROOT)" ]
GOBUILDVERSION_CURRENT := $(shell $(GOROOT)/bin/go version 2>/dev/null)
ifneq ($(GOBUILDVERSION_NEEDED),$(GOBUILDVERSION_CURRENT))
$(shell rm -f $(GOROOT)/bin/go)
endif
.cache/$(GOBUILDTARBALL):
mkdir -p $(dir $@)
curl -o $@ $(GOBUILDTARBALLURL) || { rm -f $@; exit 1; }
$(GOROOT)/bin/go: .cache/$(GOBUILDTARBALL)
rm -rf "$(GOROOT)"
mkdir -p "$(GOROOT)" mkdir -p "$(GOROOT)"
tar -C "$(GOROOT)" --strip-components=1 -xzf - < .cache/$(GOBUILDTARBALL) || { rm -rf "$(GOROOT)"; exit 1; } rsync -a --delete --exclude=pkg/obj/go-build "$(REAL_GOROOT)/" "$(GOROOT)/"
patch -p1 -f -N -r- -d "$(GOROOT)" < goruntime-boottime-over-monotonic.diff || { rm -rf "$(GOROOT)"; exit 1; } patch -p1 -f -N -r- -d "$(GOROOT)" < goruntime-boottime-over-monotonic.diff
$(shell test "$$(cat "$(BUILDDIR)/.gobuildversion" 2>/dev/null)" = "$(GOBUILDVERSION_CURRENT)" || rm -f "$(DESTDIR)/libwg-go.a")
define copy-src-to-build
$(subst $(1),$(BUILDDIR)/,$(2)): $(2)
@mkdir -vp "$$(dir $$@)"
@cp -vp "$$<" "$$@"
$(BUILDDIR)/.prepared: $(subst $(1),$(BUILDDIR)/,$(2))
endef
$(foreach FILE,$(UPSTREAM_FILES),$(eval $(call copy-src-to-build,../wireguard-go/,$(FILE))))
$(foreach FILE,$(DOWNSTREAM_FILES),$(eval $(call copy-src-to-build,src/,$(FILE))))
$(BUILDDIR)/.prepared: $(GOROOT)/bin/go
cd "$(BUILDDIR)" || exit $$?; $(foreach ARCH,$(ARCHS),CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" GOARCH="$(GOARCH_$(ARCH))" go get -tags ios || { ret=$$?; chmod -fR +w "$(GOPATH)/pkg/mod"; rm -rf "$(GOPATH)/pkg/mod"; exit $$ret; };)
chmod -fR +w "$(GOPATH)/pkg/mod"
touch "$@" touch "$@"
define libwg-go-a define libwg-go-a
$(BUILDDIR)/libwg-go-$(1).a: $(BUILDDIR)/.prepared $(BUILDDIR)/libwg-go-$(1).a: export CGO_ENABLED := 1
cd "$(BUILDDIR)" || exit $$$$?; \ $(BUILDDIR)/libwg-go-$(1).a: export CGO_CFLAGS := $(CFLAGS_PREFIX) $(ARCH)
CGO_CFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \ $(BUILDDIR)/libwg-go-$(1).a: export CGO_LDFLAGS := $(CFLAGS_PREFIX) $(ARCH)
CGO_LDFLAGS="$(CFLAGS_PREFIX) $(ARCH)" \ $(BUILDDIR)/libwg-go-$(1).a: export GOOS := darwin
GOARCH="$(GOARCH_$(1))" \ $(BUILDDIR)/libwg-go-$(1).a: export GOARCH := $(GOARCH_$(1))
go build -tags ios -ldflags=-w -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive && go version > "$(BUILDDIR)/.gobuildversion"; \ $(BUILDDIR)/libwg-go-$(1).a: $(GOROOT)/.prepared go.mod
ret=$$$$?; \ go build -tags ios -ldflags=-w -trimpath -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive
rm -f "$(BUILDDIR)/libwg-go-$(1).h"; \ rm -f "$(BUILDDIR)/libwg-go-$(1).h"
exit $$$$ret
endef endef
$(foreach ARCH,$(ARCHS),$(eval $(call libwg-go-a,$(ARCH)))) $(foreach ARCH,$(ARCHS),$(eval $(call libwg-go-a,$(ARCH))))
$(DESTDIR)/wireguard-go-version.h: ../wireguard-go/version.go $(DESTDIR)/wireguard-go-version.h: $(GOROOT)/.prepared go.mod
sed -n 's/.*WireGuardGoVersion = "\(.*\)"/#define WIREGUARD_GO_VERSION "\1"/p' "$^" > "$@" go list -m golang.zx2c4.com/wireguard | sed -n 's/.*v\([0-9.]*\).*/#define WIREGUARD_GO_VERSION "\1"/p' > "$@"
$(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a) $(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
@mkdir -vp "$(DESTDIR)" @mkdir -vp "$(DESTDIR)"
@@ -89,9 +53,6 @@ $(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
clean: clean:
rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h" rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h"
distclean: clean
rm -rf .cache
install: build install: build
.PHONY: distclean clean build version-header install .PHONY: clean build version-header install
@@ -1,4 +1,4 @@
/* SPDX-License-Identifier: GPL-2.0 /* SPDX-License-Identifier: MIT
* *
* Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. * Copyright (C) 2018-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/ */
@@ -17,8 +17,9 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"errors" "errors"
"git.zx2c4.com/wireguard-go/tun"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun"
"log" "log"
"math" "math"
"os" "os"
@@ -45,12 +46,16 @@ func (l *CLogger) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
var tunnelHandles map[int32]*Device type tunnelHandle struct {
*device.Device
*device.Logger
}
var tunnelHandles = make(map[int32]tunnelHandle)
func init() { func init() {
versionString = C.CString(WireGuardGoVersion) versionString = C.CString(device.WireGuardGoVersion)
roamingDisabled = true device.RoamingDisabled = true
tunnelHandles = make(map[int32]*Device)
signals := make(chan os.Signal) signals := make(chan os.Signal)
signal.Notify(signals, unix.SIGUSR2) signal.Notify(signals, unix.SIGUSR2)
go func() { go func() {
@@ -61,7 +66,7 @@ func init() {
n := runtime.Stack(buf, true) n := runtime.Stack(buf, true)
buf[n] = 0 buf[n] = 0
if uintptr(loggerFunc) != 0 { if uintptr(loggerFunc) != 0 {
C.callLogger(loggerFunc, 0, (*_Ctype_char)(unsafe.Pointer(&buf[0]))) C.callLogger(loggerFunc, 0, (*C.char)(unsafe.Pointer(&buf[0])))
} }
} }
} }
@@ -70,7 +75,7 @@ func init() {
//export wgEnableRoaming //export wgEnableRoaming
func wgEnableRoaming(enabled bool) { func wgEnableRoaming(enabled bool) {
roamingDisabled = !enabled device.RoamingDisabled = !enabled
} }
//export wgSetLogger //export wgSetLogger
@@ -80,21 +85,26 @@ func wgSetLogger(loggerFn uintptr) {
//export wgTurnOn //export wgTurnOn
func wgTurnOn(settings string, tunFd int32) int32 { func wgTurnOn(settings string, tunFd int32) int32 {
logger := &Logger{ logger := &device.Logger{
Debug: log.New(&CLogger{level: 0}, "", 0), Debug: log.New(&CLogger{level: 0}, "", 0),
Info: log.New(&CLogger{level: 1}, "", 0), Info: log.New(&CLogger{level: 1}, "", 0),
Error: log.New(&CLogger{level: 2}, "", 0), Error: log.New(&CLogger{level: 2}, "", 0),
} }
tun, _, err := tun.CreateTUNFromFD(int(tunFd)) err := unix.SetNonblock(int(tunFd), true)
if err != nil {
logger.Error.Println(err)
return -1
}
tun, err := tun.CreateTUNFromFile(os.NewFile(uintptr(tunFd), "/dev/tun"), 0)
if err != nil { if err != nil {
logger.Error.Println(err) logger.Error.Println(err)
return -1 return -1
} }
logger.Info.Println("Attaching to interface") logger.Info.Println("Attaching to interface")
device := NewDevice(tun, logger) device := device.NewDevice(tun, logger)
setError := ipcSetOperation(device, bufio.NewReader(strings.NewReader(settings))) setError := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings)))
if setError != nil { if setError != nil {
logger.Error.Println(setError) logger.Error.Println(setError)
return -1 return -1
@@ -112,7 +122,7 @@ func wgTurnOn(settings string, tunFd int32) int32 {
if i == math.MaxInt32 { if i == math.MaxInt32 {
return -1 return -1
} }
tunnelHandles[i] = device tunnelHandles[i] = tunnelHandle{device, logger}
return i return i
} }
@@ -132,10 +142,10 @@ func wgSetConfig(tunnelHandle int32, settings string) int64 {
if !ok { if !ok {
return 0 return 0
} }
err := ipcSetOperation(device, bufio.NewReader(strings.NewReader(settings))) err := device.IpcSetOperation(bufio.NewReader(strings.NewReader(settings)))
if err != nil { if err != nil {
device.log.Error.Println(err) device.Error.Println(err)
return err.int64 return err.ErrorCode()
} }
return 0 return 0
} }
@@ -148,57 +158,21 @@ func wgGetConfig(tunnelHandle int32) *C.char {
} }
settings := new(bytes.Buffer) settings := new(bytes.Buffer)
writer := bufio.NewWriter(settings) writer := bufio.NewWriter(settings)
err := ipcGetOperation(device, writer) err := device.IpcGetOperation(writer)
if err != nil { if err != nil {
return nil return nil
} }
writer.Flush() writer.Flush()
return C.CString(settings.String()) return C.CString(settings.String())
} }
//export wgBumpSockets
//export wgBindInterfaceScope func wgBumpSockets(tunnelHandle int32) {
func wgBindInterfaceScope(tunnelHandle int32, ifscope int32) {
var operr error
device, ok := tunnelHandles[tunnelHandle] device, ok := tunnelHandles[tunnelHandle]
if !ok { if !ok {
return return
} }
device.log.Info.Printf("Binding sockets to interface %d\n", ifscope) device.BindUpdate()
bind := device.net.bind.(*NativeBind) device.SendKeepalivesToPeersWithCurrentKeypair()
for bind.ipv4 != nil {
fd, err := bind.ipv4.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v4 socket to interface:", err)
}
break
}
for bind.ipv6 != nil {
fd, err := bind.ipv6.SyscallConn()
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
break
}
err = fd.Control(func(fd uintptr) {
operr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, int(ifscope))
})
if err == nil {
err = operr
}
if err != nil {
device.log.Error.Printf("Unable to bind v6 socket to interface:", err)
}
break
}
} }
//export wgVersion //export wgVersion
+9
View File
@@ -0,0 +1,9 @@
module golang.zx2c4.com/wireguard/ios
go 1.13
require (
golang.org/x/net v0.0.0-20191007182048-72f939374954 // indirect
golang.org/x/sys v0.0.0-20191008105621-543471e840be
golang.zx2c4.com/wireguard v0.0.20190909-0.20191008144818-222f0f8000e8
)
+20
View File
@@ -0,0 +1,20 @@
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc h1:c0o/qxkaO2LF5t6fQrT4b5hzyggAkLLlCUjqfRxd8Q4=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823 h1:Ypyv6BNJh07T1pUSrehkLemqPKXhus2MkfktJ91kRh4=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954 h1:JGZucVF/L/TotR719NbujzadOZ2AgnYlqphQGHDCKaU=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c h1:6Zx7DRlKXf79yfxuQ/7GqV3w2y7aDsk6bGg0MzF5RVU=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.20190909-0.20191008144818-222f0f8000e8 h1:BqfQHKZLrdq0j5Z/R9coISbr1nYcSE+3BdyF5LidO+g=
golang.zx2c4.com/wireguard v0.0.20190909-0.20191008144818-222f0f8000e8/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
@@ -1,55 +1,27 @@
diff -r -u go/src/runtime/sys_darwin_386.s go-modified/src/runtime/sys_darwin_386.s From 04f5695b83cd221e99e9fa6171b57e45177d5ad3 Mon Sep 17 00:00:00 2001
--- go/src/runtime/sys_darwin_386.s 2018-10-01 23:02:54.000000000 +0200 From: "Jason A. Donenfeld" <Jason@zx2c4.com>
+++ go-modified/src/runtime/sys_darwin_386.s 2018-11-01 23:18:04.383055355 +0100 Date: Wed, 27 Feb 2019 05:33:01 +0100
@@ -184,7 +184,7 @@ Subject: [PATCH] runtime: use libc_mach_continuous_time in nanotime on Darwin
PUSHL BP
MOVL SP, BP This makes timers account for having expired while a computer was
SUBL $8+(machTimebaseInfo__size+15)/16*16, SP asleep, which is quite common on mobile devices. Note that
- CALL libc_mach_absolute_time(SB) continuous_time absolute_time, except that it takes into account
+ CALL libc_mach_continuous_time(SB) time spent in suspend.
MOVL 16+(machTimebaseInfo__size+15)/16*16(SP), CX
MOVL AX, 0(CX) Fixes #24595
MOVL DX, 4(CX) ---
diff -r -u go/src/runtime/sys_darwin_amd64.s go-modified/src/runtime/sys_darwin_amd64.s src/runtime/sys_darwin.go | 2 +-
--- go/src/runtime/sys_darwin_amd64.s 2018-10-01 23:02:54.000000000 +0200 src/runtime/sys_darwin_386.s | 2 +-
+++ go-modified/src/runtime/sys_darwin_amd64.s 2018-11-01 23:18:04.382055360 +0100 src/runtime/sys_darwin_amd64.s | 2 +-
@@ -85,7 +85,7 @@ src/runtime/sys_darwin_arm.s | 2 +-
PUSHQ BP src/runtime/sys_darwin_arm64.s | 2 +-
MOVQ SP, BP 5 files changed, 5 insertions(+), 5 deletions(-)
MOVQ DI, BX
- CALL libc_mach_absolute_time(SB) diff --git a/src/runtime/sys_darwin.go b/src/runtime/sys_darwin.go
+ CALL libc_mach_continuous_time(SB) index 376f76dbc5..a0677a83f6 100644
MOVQ AX, 0(BX) --- a/src/runtime/sys_darwin.go
MOVL timebase<>+machTimebaseInfo_numer(SB), SI +++ b/src/runtime/sys_darwin.go
MOVL timebase<>+machTimebaseInfo_denom(SB), DI // atomic read @@ -431,7 +431,7 @@ func setNonblock(fd int32) {
diff -r -u go/src/runtime/sys_darwin_arm64.s go-modified/src/runtime/sys_darwin_arm64.s
--- go/src/runtime/sys_darwin_arm64.s 2018-10-01 23:02:54.000000000 +0200
+++ go-modified/src/runtime/sys_darwin_arm64.s 2018-11-01 23:18:04.380055369 +0100
@@ -110,7 +110,7 @@
TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$40
MOVD R0, R19
- BL libc_mach_absolute_time(SB)
+ BL libc_mach_continuous_time(SB)
MOVD R0, 0(R19)
MOVW timebase<>+machTimebaseInfo_numer(SB), R20
MOVD $timebase<>+machTimebaseInfo_denom(SB), R21
diff -r -u go/src/runtime/sys_darwin_arm.s go-modified/src/runtime/sys_darwin_arm.s
--- go/src/runtime/sys_darwin_arm.s 2018-10-01 23:02:54.000000000 +0200
+++ go-modified/src/runtime/sys_darwin_arm.s 2018-11-01 23:18:04.381055364 +0100
@@ -118,7 +118,7 @@
TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$0
MOVW R0, R8
- BL libc_mach_absolute_time(SB)
+ BL libc_mach_continuous_time(SB)
MOVW R0, 0(R8)
MOVW R1, 4(R8)
MOVW timebase<>+machTimebaseInfo_numer(SB), R6
diff -r -u go/src/runtime/sys_darwin.go go-modified/src/runtime/sys_darwin.go
--- go/src/runtime/sys_darwin.go 2018-10-01 23:02:54.000000000 +0200
+++ go-modified/src/runtime/sys_darwin.go 2018-11-01 23:18:04.384055350 +0100
@@ -348,7 +348,7 @@
//go:cgo_import_dynamic libc_usleep usleep "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_usleep usleep "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_mach_timebase_info mach_timebase_info "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_mach_timebase_info mach_timebase_info "/usr/lib/libSystem.B.dylib"
@@ -58,3 +30,58 @@ diff -r -u go/src/runtime/sys_darwin.go go-modified/src/runtime/sys_darwin.go
//go:cgo_import_dynamic libc_gettimeofday gettimeofday "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_gettimeofday gettimeofday "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_sigaction sigaction "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_sigaction sigaction "/usr/lib/libSystem.B.dylib"
//go:cgo_import_dynamic libc_pthread_sigmask pthread_sigmask "/usr/lib/libSystem.B.dylib" //go:cgo_import_dynamic libc_pthread_sigmask pthread_sigmask "/usr/lib/libSystem.B.dylib"
diff --git a/src/runtime/sys_darwin_386.s b/src/runtime/sys_darwin_386.s
index e653c54f61..5a43fcbdc1 100644
--- a/src/runtime/sys_darwin_386.s
+++ b/src/runtime/sys_darwin_386.s
@@ -199,7 +199,7 @@ TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$0
PUSHL BP
MOVL SP, BP
SUBL $8+(machTimebaseInfo__size+15)/16*16, SP
- CALL libc_mach_absolute_time(SB)
+ CALL libc_mach_continuous_time(SB)
MOVL 16+(machTimebaseInfo__size+15)/16*16(SP), CX
MOVL AX, 0(CX)
MOVL DX, 4(CX)
diff --git a/src/runtime/sys_darwin_amd64.s b/src/runtime/sys_darwin_amd64.s
index 87c8db8c82..f962f24339 100644
--- a/src/runtime/sys_darwin_amd64.s
+++ b/src/runtime/sys_darwin_amd64.s
@@ -97,7 +97,7 @@ TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$0
PUSHQ BP
MOVQ SP, BP
MOVQ DI, BX
- CALL libc_mach_absolute_time(SB)
+ CALL libc_mach_continuous_time(SB)
MOVQ AX, 0(BX)
MOVL timebase<>+machTimebaseInfo_numer(SB), SI
MOVL timebase<>+machTimebaseInfo_denom(SB), DI // atomic read
diff --git a/src/runtime/sys_darwin_arm.s b/src/runtime/sys_darwin_arm.s
index 996f8028a3..5bd34b51be 100644
--- a/src/runtime/sys_darwin_arm.s
+++ b/src/runtime/sys_darwin_arm.s
@@ -126,7 +126,7 @@ GLOBL timebase<>(SB),NOPTR,$(machTimebaseInfo__size)
TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$0
MOVW R0, R8
- BL libc_mach_absolute_time(SB)
+ BL libc_mach_continuous_time(SB)
MOVW R0, 0(R8)
MOVW R1, 4(R8)
MOVW timebase<>+machTimebaseInfo_numer(SB), R6
diff --git a/src/runtime/sys_darwin_arm64.s b/src/runtime/sys_darwin_arm64.s
index ac3ca74f63..5e91540f94 100644
--- a/src/runtime/sys_darwin_arm64.s
+++ b/src/runtime/sys_darwin_arm64.s
@@ -121,7 +121,7 @@ GLOBL timebase<>(SB),NOPTR,$(machTimebaseInfo__size)
TEXT runtime·nanotime_trampoline(SB),NOSPLIT,$40
MOVD R0, R19
- BL libc_mach_absolute_time(SB)
+ BL libc_mach_continuous_time(SB)
MOVD R0, 0(R19)
MOVW timebase<>+machTimebaseInfo_numer(SB), R20
MOVD $timebase<>+machTimebaseInfo_denom(SB), R21
--
2.23.0
-16
View File
@@ -1,16 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2017-2019 WireGuard LLC. All Rights Reserved.
*/
package main
/* Fit within memory limits for iOS */
const (
QueueOutboundSize = 1024
QueueInboundSize = 1024
QueueHandshakeSize = 1024
MaxSegmentSize = 1700
PreallocatedBuffersPerPool = 1024
)
-52
View File
@@ -1,52 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0
*
* Copyright (C) 2017-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
*/
package tun
import (
"git.zx2c4.com/wireguard-go/rwcancel"
"golang.org/x/sys/unix"
"net"
"os"
)
func CreateTUNFromFD(tunFd int) (TUNDevice, string, error) {
file := os.NewFile(uintptr(tunFd), "/dev/tun")
tun := &nativeTun{
tunFile: file,
fd: file.Fd(),
events: make(chan TUNEvent, 5),
errors: make(chan error, 5),
}
var err error
tun.rwcancel, err = rwcancel.NewRWCancel(tunFd)
if err != nil {
return nil, "", err
}
name, err := tun.Name()
if err != nil {
tun.rwcancel.Cancel()
return nil, "", err
}
tunIfindex, err := func() (int, error) {
iface, err := net.InterfaceByName(name)
if err != nil {
return -1, err
}
return iface.Index, nil
}()
if err != nil {
tun.tunFile.Close()
return nil, "", err
}
tun.routeSocket, err = unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
if err != nil {
tun.tunFile.Close()
return nil, "", err
}
go tun.routineRouteListener(tunIfindex)
return tun, name, nil
}

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