diff --git a/WireGuard/WireGuard.xcodeproj/project.pbxproj b/WireGuard/WireGuard.xcodeproj/project.pbxproj index 1b6ceb0..c93cef2 100644 --- a/WireGuard/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard/WireGuard.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 6FDEF80021863C0100D8FBF6 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */; }; 6FDEF802218646BA00D8FBF6 /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */; }; 6FDEF806218725D200D8FBF6 /* SettingsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */; }; + 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */; }; 6FF4AC1F211EC472002C96EB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC1E211EC472002C96EB /* Assets.xcassets */; }; 6FF4AC22211EC472002C96EB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6FF4AC20211EC472002C96EB /* LaunchScreen.storyboard */; }; 6FF4AC472120B9E0002C96EB /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FF4AC462120B9E0002C96EB /* NetworkExtension.framework */; }; @@ -105,6 +106,7 @@ 6FDEF7FF21863C0100D8FBF6 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ioapi.c; sourceTree = ""; }; 6FDEF801218646B900D8FBF6 /* ZipArchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; 6FDEF805218725D200D8FBF6 /* SettingsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsTableViewController.swift; sourceTree = ""; }; + 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WgQuickConfigFileWriter.swift; sourceTree = ""; }; 6FF4AC14211EC46F002C96EB /* WireGuard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WireGuard.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6FF4AC1E211EC472002C96EB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 6FF4AC21211EC472002C96EB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -167,6 +169,7 @@ isa = PBXGroup; children = ( 6F6899AB218099F00012E523 /* WgQuickConfigFileParser.swift */, + 6FDEF8072187442100D8FBF6 /* WgQuickConfigFileWriter.swift */, ); path = ConfigFile; sourceTree = ""; @@ -445,6 +448,7 @@ 6FDEF7FC21863B6100D8FBF6 /* zip.c in Sources */, 6F628C3F217F3413003482A3 /* DNSServer.swift in Sources */, 6F628C3D217F09E9003482A3 /* TunnelViewModel.swift in Sources */, + 6FDEF8082187442100D8FBF6 /* WgQuickConfigFileWriter.swift in Sources */, 6F7774EA217229DB006A79B3 /* IPAddressRange.swift in Sources */, 6F7774E82172020C006A79B3 /* Configuration.swift in Sources */, 6FDEF7FB21863B6100D8FBF6 /* unzip.c in Sources */, diff --git a/WireGuard/WireGuard/ConfigFile/WgQuickConfigFileWriter.swift b/WireGuard/WireGuard/ConfigFile/WgQuickConfigFileWriter.swift new file mode 100644 index 0000000..23095d3 --- /dev/null +++ b/WireGuard/WireGuard/ConfigFile/WgQuickConfigFileWriter.swift @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// Copyright © 2018 WireGuard LLC. All rights reserved. + +import UIKit + +class WgQuickConfigFileWriter { + static func writeConfigFile(from tc: TunnelConfiguration) -> Data? { + let interface = tc.interface + var output = "[Interface]\n" + output.append("PrivateKey=\(interface.privateKey.base64EncodedString())\n") + if let listenPort = interface.listenPort { + output.append("ListenPort=\(listenPort)\n") + } + if (!interface.addresses.isEmpty) { + let addressString = interface.addresses.map { $0.stringRepresentation() }.joined(separator: ", ") + output.append("Address=\(addressString)\n") + } + if (!interface.dns.isEmpty) { + let dnsString = interface.dns.map { $0.stringRepresentation() }.joined(separator: ", ") + output.append("DNS=\(dnsString)\n") + } + if let mtu = interface.mtu { + output.append("MTU=\(mtu)\n") + } + output.append("\n") + + for peer in tc.peers { + output.append("[Peers]\n") + output.append("PublicKey=\(peer.publicKey.base64EncodedString())\n") + if let preSharedKey = peer.preSharedKey { + output.append("PresharedKey=\(preSharedKey.base64EncodedString())\n") + } + if (!peer.allowedIPs.isEmpty) { + let allowedIPsString = peer.allowedIPs.map { $0.stringRepresentation() }.joined(separator: ", ") + output.append("AllowedIPs=\(allowedIPsString)\n") + } + if let endpoint = peer.endpoint { + output.append("Endpoint=\(endpoint.stringRepresentation())\n") + } + if let persistentKeepAlive = peer.persistentKeepAlive { + output.append("PersistentKeepalive=\(persistentKeepAlive)\n") + } + output.append("\n") + } + + return output.data(using: .utf8) + } +} diff --git a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift index 8cbaa2d..e4b7fa9 100644 --- a/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/SettingsTableViewController.swift @@ -2,6 +2,7 @@ // Copyright © 2018 WireGuard LLC. All rights reserved. import UIKit +import os.log class SettingsTableViewController: UITableViewController { @@ -16,7 +17,10 @@ class SettingsTableViewController: UITableViewController { [.exportZipArchive] ] - init() { + let tunnelsManager: TunnelsManager? + + init(tunnelsManager: TunnelsManager?) { + self.tunnelsManager = tunnelsManager super.init(style: .grouped) } @@ -39,6 +43,65 @@ class SettingsTableViewController: UITableViewController { @objc func doneTapped() { dismiss(animated: true, completion: nil) } + + func exportConfigurationsAsZipFile(sourceView: UIView) { + guard let tunnelsManager = tunnelsManager, tunnelsManager.numberOfTunnels() > 0 else { + showErrorAlert(title: "Nothing to export", message: "There are no tunnel configurations to export") + return + } + var inputsToArchiver: [(fileName: String, contents: Data)] = [] + var usedNames: Set = [] + for i in 0 ..< tunnelsManager.numberOfTunnels() { + guard let tunnelConfiguration = tunnelsManager.tunnel(at: i).tunnelConfiguration() else { continue } + if let contents = WgQuickConfigFileWriter.writeConfigFile(from: tunnelConfiguration) { + let name = tunnelConfiguration.interface.name + var nameToCheck = name + var i = 0 + while (usedNames.contains(nameToCheck)) { + i = i + 1 + nameToCheck = "\(name)\(i)" + } + usedNames.insert(nameToCheck) + inputsToArchiver.append((fileName: "\(nameToCheck).conf", contents: contents)) + } + } + + // Based on file export code by Jeroen Leenarts in commit ca35168 + guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + return + } + let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip") + do { + try FileManager.default.removeItem(at: destinationURL) + } catch { + os_log("Failed to delete file: %{public}@ : %{public}@", log: OSLog.default, type: .error, destinationURL.absoluteString, error.localizedDescription) + } + + var ok = false + do { + try ZipArchive.archive(inputs: inputsToArchiver, to: destinationURL) + ok = true + } catch { + os_log("Failed to create archive: %{public}@ : %{public}@", log: OSLog.default, type: .error, destinationURL.absoluteString) + } + + if (ok) { + let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil) + // popoverPresentationController shall be non-nil on the iPad + activityVC.popoverPresentationController?.sourceView = sourceView + present(activityVC, animated: true) + } else { + showErrorAlert(title: "Could not export", message: "There was an error creating the tunnel configuration archive") + } + } + + func showErrorAlert(title: String, message: String) { + let okAction = UIAlertAction(title: "Ok", style: .default) + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(okAction) + + self.present(alert, animated: true, completion: nil) + } } // MARK: UITableViewDataSource @@ -79,6 +142,9 @@ extension SettingsTableViewController { assert(field == .exportZipArchive) let cell = tableView.dequeueReusableCell(withIdentifier: TunnelSettingsTableViewButtonCell.id, for: indexPath) as! TunnelSettingsTableViewButtonCell cell.buttonText = field.rawValue + cell.onTapped = { [weak self] in + self?.exportConfigurationsAsZipFile(sourceView: cell.button) + } return cell } } diff --git a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift index 2533a16..fa8e3ce 100644 --- a/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift +++ b/WireGuard/WireGuard/UI/iOS/TunnelsListTableViewController.swift @@ -71,7 +71,7 @@ class TunnelsListTableViewController: UITableViewController { } @objc func settingsButtonTapped(sender: UIBarButtonItem!) { - let settingsVC = SettingsTableViewController() + let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager) let settingsNC = UINavigationController(rootViewController: settingsVC) settingsNC.modalPresentationStyle = .formSheet self.present(settingsNC, animated: true) diff --git a/WireGuard/WireGuard/ZipArchive/ZipArchive.swift b/WireGuard/WireGuard/ZipArchive/ZipArchive.swift index ea6df17..0681588 100644 --- a/WireGuard/WireGuard/ZipArchive/ZipArchive.swift +++ b/WireGuard/WireGuard/ZipArchive/ZipArchive.swift @@ -5,11 +5,29 @@ import Foundation enum ZipArchiveError: Error { case cantOpenInputZipFile + case cantOpenOutputZipFileForWriting case badArchive } class ZipArchive { + static func archive(inputs: [(fileName: String, contents: Data)], to destinationURL: URL) throws { + let destinationPath = destinationURL.path + guard let zipFile = zipOpen(destinationPath, APPEND_STATUS_CREATE) else { + throw ZipArchiveError.cantOpenOutputZipFileForWriting + } + for input in inputs { + let fileName = input.fileName + let contents = input.contents + zipOpenNewFileInZip(zipFile, fileName.cString(using: .utf8), nil, nil, 0, nil, 0, nil, Z_DEFLATED, Z_DEFAULT_COMPRESSION) + contents.withUnsafeBytes { (ptr: UnsafePointer) -> Void in + zipWriteInFileInZip(zipFile, UnsafeRawPointer(ptr), UInt32(contents.count)) + } + zipCloseFileInZip(zipFile) + } + zipClose(zipFile, nil) + } + static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileName: String, contents: Data)] { var results: [(fileName: String, contents: Data)] = []