Added ability to add tunnels with a QR code scan. Logic in place to parse conf files as well.

Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
This commit is contained in:
Eric Kuck 2018-08-21 11:00:41 -05:00
parent c2b591cc44
commit 39ae9db11c
10 changed files with 325 additions and 8 deletions

View File

@ -46,6 +46,8 @@
5FA1D4CD2124A05C00DBA2E6 /* Interface+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */; };
5FA1D5102124D80C00DBA2E6 /* String+Arrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */; };
5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */; };
5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */; };
5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4346212B3E2C009A9C58 /* Attribute.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -126,6 +128,8 @@
5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interface+Extension.swift"; sourceTree = "<group>"; };
5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Arrays.swift"; sourceTree = "<group>"; };
5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Base64.swift"; sourceTree = "<group>"; };
5FCC4342212B3092009A9C58 /* QRScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; };
5FCC4346212B3E2C009A9C58 /* Attribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attribute.swift; sourceTree = "<group>"; };
861983CAE8FDC13BC83E7E04 /* Pods_WireGuard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WireGuard.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
@ -235,6 +239,7 @@
4A8AABD720B6A79100B6D8C1 /* UITableView+WireGuard.swift */,
4A4BACE720B5F1BF00F12B28 /* TunnelsTableViewController.swift */,
4A4BA6D720B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift */,
5FCC4342212B3092009A9C58 /* QRScanViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -252,6 +257,7 @@
4A4BAD1920B5F8FF00F12B28 /* Tunnel+CoreDataProperties.swift */,
4AC5462D2116306F00749D21 /* Tunnel+Extension.swift */,
4A4BAD1520B5F8DE00F12B28 /* WireGuard.xcdatamodeld */,
5FCC4346212B3E2C009A9C58 /* Attribute.swift */,
);
path = Models;
sourceTree = "<group>";
@ -550,6 +556,7 @@
4AEAC32B20F14BA9007B67AB /* Log.swift in Sources */,
4A4BAD1320B5F82400F12B28 /* Identifyable.swift in Sources */,
4A4BAD1720B5F8DE00F12B28 /* WireGuard.xcdatamodeld in Sources */,
5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */,
4A4BAD1A20B5F8FF00F12B28 /* Tunnel+CoreDataClass.swift in Sources */,
4A4BACE820B5F1BF00F12B28 /* TunnelsTableViewController.swift in Sources */,
4A4BAD1020B5F6EC00F12B28 /* RootCoordinator.swift in Sources */,
@ -557,6 +564,7 @@
5FA1D4CB21249F7D00DBA2E6 /* Peer+Extension.swift in Sources */,
5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */,
4AC5462E2116306F00749D21 /* Tunnel+Extension.swift in Sources */,
5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */,
4A4BAD0E20B5F6C300F12B28 /* Coordinator.swift in Sources */,
4A4BA6D820B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift in Sources */,
4A4BAD2020B6026900F12B28 /* Peer+CoreDataProperties.swift in Sources */,

View File

@ -6,6 +6,7 @@
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -73,12 +74,12 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4uZ-Vv-Fry" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="34" y="154"/>
<point key="canvasLocation" x="-670" y="200"/>
</scene>
<!--Tunnel settings-->
<scene sceneID="xV8-BW-4R7">
<objects>
<tableViewController storyboardIdentifier="TunnelConfigurationTableViewController" id="0VM-73-EPX" customClass="TunnelConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="TunnelConfigurationTableViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0VM-73-EPX" customClass="TunnelConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="0Uy-k2-O3i">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -472,7 +473,37 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="j96-PK-ghN" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1128.8" y="321.58920539730138"/>
<point key="canvasLocation" x="122" y="-239"/>
</scene>
<!--Scan Code-->
<scene sceneID="gKN-k2-HoW">
<objects>
<viewController storyboardIdentifier="QRScanViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Efe-yN-iDH" customClass="QRScanViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="NXo-On-ea8">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: generate with `qrencode -t ansiutf8 &lt; tunnel.conf`" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XYc-tx-YNF">
<rect key="frame" x="16" y="628.5" width="343" height="14.5"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Soo-c9-MsX" firstAttribute="bottom" secondItem="XYc-tx-YNF" secondAttribute="bottom" constant="24" id="QhS-p5-jbw"/>
<constraint firstItem="XYc-tx-YNF" firstAttribute="leading" secondItem="NXo-On-ea8" secondAttribute="leading" constant="16" id="Y3P-Py-ueV"/>
<constraint firstAttribute="trailing" secondItem="XYc-tx-YNF" secondAttribute="trailing" constant="16" id="sB1-h9-ueh"/>
</constraints>
<viewLayoutGuide key="safeArea" id="Soo-c9-MsX"/>
</view>
<navigationItem key="navigationItem" title="Scan Code" largeTitleDisplayMode="never" id="WGY-tY-ySz"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="TQ2-zp-o40" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="121" y="461"/>
</scene>
</scenes>
<resources>

View File

@ -32,7 +32,6 @@ class AppCoordinator: RootViewCoordinator {
return self.tunnelsTableViewController
}
var tunnelsTableViewController: TunnelsTableViewController!
/// Window to manage
@ -134,10 +133,33 @@ class AppCoordinator: RootViewCoordinator {
extension AppCoordinator: TunnelsTableViewControllerDelegate {
func addProvider(tunnelsTableViewController: TunnelsTableViewController) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in
self.addProviderManually()
})
actionSheet.addAction(UIAlertAction(title: "Scan QR Code", style: .default) { [unowned self] _ in
self.addProviderWithQRScan()
})
actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
tunnelsTableViewController.present(actionSheet, animated: true, completion: nil)
}
func addProviderManually() {
let addContext = persistentContainer.newBackgroundContext()
showTunnelConfigurationViewController(tunnel: nil, context: addContext)
}
func addProviderWithQRScan() {
let addContext = persistentContainer.newBackgroundContext()
let qrScanViewController = storyboard.instantiateViewController(type: QRScanViewController.self)
qrScanViewController.configure(context: addContext, delegate: self)
self.navigationController.pushViewController(qrScanViewController, animated: true)
}
func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) {
let manager = self.providerManager(for: tunnel)!
let block = {
@ -237,10 +259,8 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate {
return tunnelIdentifier == tunnel.tunnelIdentifier
}
}
}
extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate {
func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) {
private func saveTunnel(_ tunnel: Tunnel) {
let manager = providerManager(for: tunnel) ?? NETunnelProviderManager()
manager.localizedDescription = tunnel.title
@ -265,5 +285,18 @@ extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate {
navigationController.popToRootViewController(animated: true)
}
}
extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate {
func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) {
saveTunnel(tunnel)
}
}
extension AppCoordinator: QRScanViewControllerDelegate {
func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController) {
showTunnelConfigurationViewController(tunnel: tunnel, context: tunnel.managedObjectContext!)
}
}

View File

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Camera is used to scan QR codes</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>

View File

@ -0,0 +1,44 @@
//
// Attribute.swift
// WireGuard
//
// Created by Eric Kuck on 8/20/18.
// Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All rights reserved.
//
import Foundation
struct Attribute {
enum Key: String, CaseIterable {
case address = "Address"
case allowedIPs = "AllowedIPs"
case dns = "DNS"
case endpoint = "Endpoint"
case listenPort = "ListenPort"
case mtu = "MTU"
case persistentKeepalive = "PersistentKeepalive"
case presharedKey = "PresharedKey"
case privateKey = "PrivateKey"
case publicKey = "PublicKey"
}
private static let separatorPattern = (try? NSRegularExpression(pattern: "\\s|=", options: []))!
let line: String
let key: Key
let stringValue: String
var arrayValue: [String] {
return stringValue.commaSeparatedToArray()
}
static func match(line: String) -> Attribute? {
guard let equalsIndex = line.firstIndex(of: "=") else { return nil }
let keyString = line[..<equalsIndex].trimmingCharacters(in: .whitespaces)
let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces)
guard let key = Key.allCases.first(where: { $0.rawValue.lowercased() == keyString.lowercased() }) else { return nil }
return Attribute(line: line, key: key, stringValue: value)
}
}

View File

@ -36,6 +36,27 @@ extension Interface {
}
}
func parse(attribute: Attribute) throws {
switch attribute.key {
case .address:
addresses = attribute.stringValue
case .dns:
dns = attribute.stringValue
case .listenPort:
if let port = Int16(attribute.stringValue) {
listenPort = port
}
case .mtu:
if let mtu = Int32(attribute.stringValue) {
self.mtu = mtu
}
case .privateKey:
privateKey = attribute.stringValue
default:
throw TunnelParseError.invalidLine(attribute.line)
}
}
}
enum InterfaceValidationError: Error {

View File

@ -44,6 +44,25 @@ extension Peer {
}
}
func parse(attribute: Attribute) throws {
switch attribute.key {
case .allowedIPs:
allowedIPs = attribute.stringValue
case .endpoint:
endpoint = attribute.stringValue
case .persistentKeepalive:
if let keepAlive = Int32(attribute.stringValue) {
persistentKeepalive = keepAlive
}
case .presharedKey:
presharedKey = attribute.stringValue
case .publicKey:
publicKey = attribute.stringValue
default:
throw TunnelParseError.invalidLine(attribute.line)
}
}
}
enum PeerValidationError: Error {

View File

@ -111,6 +111,54 @@ extension Tunnel {
}
}
static func fromConfig(_ text: String, context: NSManagedObjectContext) throws -> Tunnel {
let lines = text.split(separator: "\n")
var currentPeer: Peer?
var isInInterfaceSection = false
var tunnel: Tunnel!
context.performAndWait {
tunnel = Tunnel(context: context)
tunnel.interface = Interface(context: context)
}
tunnel.tunnelIdentifier = UUID().uuidString
for line in lines {
var trimmedLine: String
if let commentRange = line.range(of: "#") {
trimmedLine = String(line[..<commentRange.lowerBound])
} else {
trimmedLine = String(line)
}
trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces)
guard trimmedLine.count > 0 else { continue }
if "[interface]" == line.lowercased() {
currentPeer = nil
isInInterfaceSection = true
} else if "[peer]" == line.lowercased() {
context.performAndWait { currentPeer = Peer(context: context) }
tunnel.insertIntoPeers(currentPeer!, at: tunnel.peers?.count ?? 0)
isInInterfaceSection = false
} else if isInInterfaceSection, let attribute = Attribute.match(line: String(line)) {
try tunnel.interface!.parse(attribute: attribute)
} else if let currentPeer = currentPeer, let attribute = Attribute.match(line: String(line)) {
try currentPeer.parse(attribute: attribute)
} else {
throw TunnelParseError.invalidLine(String(line))
}
}
if !isInInterfaceSection && currentPeer == nil {
throw TunnelParseError.noConfigInfo
}
return tunnel
}
}
private func base64KeyToHex(_ base64: String?) -> String? {
@ -146,3 +194,8 @@ enum TunnelValidationError: Error {
case nilPeers
case invalidPeer
}
enum TunnelParseError: Error {
case invalidLine(_ line: String)
case noConfigInfo
}

View File

@ -0,0 +1,107 @@
//
// QRScanViewController.swift
// WireGuard
//
// Created by Eric Kuck on 8/20/18.
// Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All rights reserved.
//
import AVFoundation
import CoreData
import UIKit
protocol QRScanViewControllerDelegate: class {
func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController)
}
class QRScanViewController: UIViewController {
private var viewContext: NSManagedObjectContext!
private weak var delegate: QRScanViewControllerDelegate?
var captureSession: AVCaptureSession? = AVCaptureSession()
let metadataOutput = AVCaptureMetadataOutput()
var previewLayer: AVCaptureVideoPreviewLayer!
func configure(context: NSManagedObjectContext, delegate: QRScanViewControllerDelegate? = nil) {
viewContext = context
self.delegate = delegate
}
override func viewDidLoad() {
super.viewDidLoad()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
let captureSession = captureSession,
captureSession.canAddInput(videoInput),
captureSession.canAddOutput(metadataOutput) else {
scanDidEncounterError(title: "Scanning Not Supported", message: "This device does not have the ability to scan QR codes.")
return
}
captureSession.addInput(videoInput)
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
metadataOutput.metadataObjectTypes = [.qr]
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if captureSession?.isRunning == false {
captureSession?.startRunning()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if captureSession?.isRunning == true {
captureSession?.stopRunning()
}
}
func scanDidComplete(withCode code: String) {
do {
let tunnel = try Tunnel.fromConfig(code, context: viewContext)
delegate?.didSave(tunnel: tunnel, qrScanViewController: self)
} catch {
scanDidEncounterError(title: "Invalid Code", message: "The scanned code is not a valid WireGuard config file.")
}
}
func scanDidEncounterError(title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
}))
present(alertController, animated: true)
captureSession = nil
}
}
extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
captureSession?.stopRunning()
guard let metadataObject = metadataObjects.first,
let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue else {
scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read.")
return
}
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
scanDidComplete(withCode: stringValue)
}
}
extension QRScanViewController: Identifyable {}

View File

@ -27,7 +27,6 @@ class TunnelConfigurationTableViewController: UITableViewController {
viewContext = context
self.delegate = delegate
self.tunnel = tunnel ?? generateNewTunnelConfig()
}
private func generateNewTunnelConfig() -> Tunnel {