Sanitised

This commit is contained in:
Neil Alexander 2020-07-19 14:20:15 +01:00
commit 52ca049b50
39 changed files with 3283 additions and 0 deletions

View file

@ -0,0 +1,312 @@
//
// PeersViewController.swift
// YggdrasilNetwork
//
// Created by Neil Alexander on 07/01/2019.
//
import UIKit
import NetworkExtension
import CoreTelephony
class PeersViewController: UITableViewController {
var app = UIApplication.shared.delegate as! AppDelegate
var config: [String: Any]? = nil
@IBOutlet var peerTable: UITableView!
@IBOutlet weak var addButtonItem: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
if let proto = self.app.vpnManager.protocolConfiguration as? NETunnelProviderProtocol {
config = proto.providerConfiguration ?? nil
}
self.navigationItem.rightBarButtonItems = [
self.editButtonItem,
self.addButtonItem
]
}
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilPeersUpdated), name: NSNotification.Name.YggdrasilPeersUpdated, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.YggdrasilPeersUpdated, object: nil)
}
@objc func onYggdrasilPeersUpdated(notification: NSNotification) {
peerTable.reloadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return app.yggdrasilSwitchPeers.count
case 1:
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
return peers.count
}
}
return 0
case 2:
if UIDevice.current.hasCellularCapabilites {
return 3
}
return 2
default: return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "discoveredPeerPrototype", for: indexPath)
let peers = app.yggdrasilSwitchPeers.sorted { (a, b) -> Bool in
return (a["Port"] as! Int) < (b["Port"] as! Int)
}
if indexPath.row < peers.count {
let value = peers[indexPath.row]
let proto = value["Protocol"] as? String ?? "tcp"
let sent = value["BytesSent"] as? Double ?? 0
let recvd = value["BytesRecvd"] as? Double ?? 0
let rx = self.format(bytes: sent)
let tx = self.format(bytes: recvd)
cell.textLabel?.text = "\(value["Endpoint"] ?? "unknown")"
cell.detailTextLabel?.text = "\(proto.uppercased()) peer on port \(value["Port"] ?? "unknown"), sent \(tx), received \(rx)"
}
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "configuredPeerPrototype", for: indexPath)
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
cell.textLabel?.text = peers[indexPath.last!]
} else {
cell.textLabel?.text = "(unknown)"
}
}
return cell
case 2:
switch indexPath.last {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "togglePrototype", for: indexPath) as! ToggleTableViewCell
cell.isUserInteractionEnabled = true
cell.label?.text = "Search for multicast peers"
cell.label?.isEnabled = true
cell.toggle?.addTarget(self, action: #selector(toggledMulticast), for: .valueChanged)
cell.toggle?.isEnabled = true
if let config = self.app.yggdrasilConfig {
let interfaces = config.get("MulticastInterfaces") as? [String] ?? []
cell.toggle?.isOn = interfaces.contains("en*")
}
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "togglePrototype", for: indexPath) as! ToggleTableViewCell
cell.isUserInteractionEnabled = false
cell.label?.text = "Search for nearby iOS peers"
cell.label?.isEnabled = false
cell.toggle?.addTarget(self, action: #selector(toggledAWDL), for: .valueChanged)
cell.toggle?.setOn(false, animated: false)
cell.toggle?.isEnabled = false
/*if let config = self.app.yggdrasilConfig {
let interfaces = config.get("MulticastInterfaces") as? [String] ?? []
cell.toggle?.isOn = interfaces.contains("awdl0")
}*/
return cell
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "menuPrototype", for: indexPath)
cell.isUserInteractionEnabled = true
cell.textLabel?.text = "Device settings"
cell.textLabel?.isEnabled = true
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "menuPrototype", for: indexPath)
cell.isUserInteractionEnabled = false
cell.textLabel?.text = "Unknown"
cell.textLabel?.isEnabled = true
return cell
}
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "configuredPeerPrototype", for: indexPath)
cell.textLabel?.text = "(unknown)"
return cell
}
}
func format(bytes: Double) -> String {
guard bytes > 0 else {
return "0 bytes"
}
// Adapted from http://stackoverflow.com/a/18650828
let suffixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
let k: Double = 1000
let i = floor(log(bytes) / log(k))
// Format number with thousands separator and everything below 1 GB with no decimal places.
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1
numberFormatter.numberStyle = .decimal
let numberString = numberFormatter.string(from: NSNumber(value: bytes / pow(k, i))) ?? "Unknown"
let suffix = suffixes[Int(i)]
return "\(numberString) \(suffix)"
}
@objc func toggledMulticast(_ sender: UISwitch) {
if let config = self.app.yggdrasilConfig {
var interfaces = config.get("MulticastInterfaces") as! [String]
if sender.isOn {
interfaces.append("en*")
} else {
interfaces.removeAll(where: { $0 == "en*" })
}
config.set("MulticastInterfaces", to: interfaces as [String])
try? config.save(to: &app.vpnManager)
}
}
@objc func toggledAWDL(_ sender: UISwitch) {
if let config = self.app.yggdrasilConfig {
var interfaces = config.get("MulticastInterfaces") as! [String]
if sender.isOn {
interfaces.append("awdl0")
} else {
interfaces.removeAll(where: { $0 == "awdl0" })
}
config.set("MulticastInterfaces", to: interfaces as [String])
try? config.save(to: &app.vpnManager)
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
if self.app.yggdrasilPeers.count > 0 {
return "Connected Peers"
}
return "No peers currently connected"
case 1:
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
if peers.count > 0 {
return "Configured Peers"
}
}
}
return "No peers currently configured"
case 2:
return "Peer Connectivity"
default: return "(Unknown)"
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch section {
case 1:
return "Yggdrasil will automatically attempt to connect to configured peers when started."
case 2:
var str = "Multicast peers will be discovered on the same Wi-Fi network or via USB."
if UIDevice.current.hasCellularCapabilites {
str += " Data charges may apply when using mobile data. You can prevent mobile data usage in the device settings."
}
return str
default: return nil
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return indexPath.first == 1
}
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
switch indexPath.first {
case 0:
return [UITableViewRowAction(style: UITableViewRowAction.Style.default, title: "Disconnect", handler: { (action, index) in
})]
case 1:
return [UITableViewRowAction(style: UITableViewRowAction.Style.normal, title: "Remove", handler: { (action, index) in
print(action, index)
if let config = self.app.yggdrasilConfig {
config.remove(index: index.last!, from: "Peers")
do {
try config.save(to: &self.app.vpnManager)
tableView.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.automatic)
} catch {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
print("Error removing: \(error)")
}
}
})]
default:
return []
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.first {
case 2:
if let last = indexPath.last, last == 2 {
UIApplication.shared.open(NSURL(string:UIApplication.openSettingsURLString)! as URL, options: [:]) { (result) in
NSLog("Result " + result.description)
}
}
default:
break
}
tableView.deselectRow(at: indexPath, animated: true)
}
@IBAction func addNewPeerButtonPressed(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "Add Configured Peer", message: """
Enter the full URI of the peer to add. Yggdrasil will automatically connect to this peer when started.
""", preferredStyle: UIAlertController.Style.alert)
let action = UIAlertAction(title: "Add", style: .default) { (alertAction) in
let textField = alert.textFields![0] as UITextField
if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String], !peers.contains(text) {
config.add(text, in: "Peers")
do {
try config.save(to: &self.app.vpnManager)
if let index = config.get("Peers") as? [String] {
self.peerTable.insertRows(at: [IndexPath(indexes: [1, index.count-1])], with: .automatic)
self.peerTable.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.automatic)
}
} catch {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
print("Add error: \(error)")
}
} else {
let alert = UIAlertController(title: "Error", message: "Peer already exists", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
}
}
}
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
alert.addTextField { (textField) in
textField.placeholder = "tcp://hostname:port"
}
alert.addAction(action)
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
}

View file

@ -0,0 +1,159 @@
//
// SettingsTableViewController.swift
// YggdrasilNetwork
//
// Created by Neil Alexander on 03/01/2019.
//
import UIKit
import NetworkExtension
class SettingsViewController: UITableViewController, UIDocumentBrowserViewControllerDelegate {
var app = UIApplication.shared.delegate as! AppDelegate
@IBOutlet weak var deviceNameField: UITextField!
@IBOutlet weak var encryptionPublicKeyLabel: UILabel!
@IBOutlet weak var signingPublicKeyLabel: UILabel!
@IBOutlet weak var autoStartWiFiCell: UITableViewCell!
@IBOutlet weak var autoStartMobileCell: UITableViewCell!
@IBOutlet weak var sessionFirewallPeeredCell: UITableViewCell!
@IBOutlet weak var sessionFirewallOtherCell: UITableViewCell!
@IBOutlet weak var sessionFirewallOutboundCell: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
if let config = self.app.yggdrasilConfig {
deviceNameField.text = config.get("name", inSection: "NodeInfo") as? String ?? ""
encryptionPublicKeyLabel.text = config.get("EncryptionPublicKey") as? String ?? "Unknown"
signingPublicKeyLabel.text = config.get("SigningPublicKey") as? String ?? "Unknown"
sessionFirewallPeeredCell.accessoryType = config.get("AllowFromDirect", inSection: "SessionFirewall") as? Bool ?? true ? .checkmark : .none
sessionFirewallOtherCell.accessoryType = config.get("AllowFromRemote", inSection: "SessionFirewall") as? Bool ?? true ? .checkmark : .none
sessionFirewallOutboundCell.accessoryType = config.get("AlwaysAllowOutbound", inSection: "SessionFirewall") as? Bool ?? true ? .checkmark : .none
autoStartWiFiCell.accessoryType = config.get("WiFi", inSection: "AutoStart") as? Bool ?? false ? .checkmark : .none
autoStartMobileCell.accessoryType = config.get("Mobile", inSection: "AutoStart") as? Bool ?? false ? .checkmark : .none
}
}
@IBAction func deviceNameEdited(_ sender: UITextField) {
if let config = self.app.yggdrasilConfig {
config.set("name", inSection: "NodeInfo", to: sender.text)
try? config.save(to: &app.vpnManager)
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.first {
case 1:
let settings = [
"WiFi",
"Mobile"
]
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = cell.accessoryType == .checkmark ? .none : .checkmark
if let config = self.app.yggdrasilConfig {
config.set(settings[indexPath.last!], inSection: "AutoStart", to: cell.accessoryType == .checkmark)
try? config.save(to: &app.vpnManager)
}
}
case 2:
let settings = [
"AllowFromDirect",
"AllowFromRemote",
"AlwaysAllowOutbound"
]
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = cell.accessoryType == .checkmark ? .none : .checkmark
if let config = self.app.yggdrasilConfig {
config.set(settings[indexPath.last!], inSection: "SessionFirewall", to: cell.accessoryType == .checkmark)
try? config.save(to: &app.vpnManager)
}
}
case 4:
switch indexPath.last {
case 0: // import
if #available(iOS 11.0, *) {
let open = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes: ["eu.neilalexander.yggdrasil.configuration"])
open.delegate = self
open.allowsDocumentCreation = false
open.allowsPickingMultipleItems = false
open.additionalTrailingNavigationBarButtonItems = [ UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelDocumentBrowser)) ]
self.present(open, animated: true, completion: nil)
} else {
let alert = UIAlertController(title: "Import Configuration", message: "Not supported on this version of iOS!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
}
case 1: // export
if let config = self.app.yggdrasilConfig, let data = config.data() {
var fileURL: URL?
var fileDir: URL?
do {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = dateFormatter.string(from: Date())
fileDir = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
fileURL = fileDir?.appendingPathComponent("Yggdrasil Backup \(date).yggconf")
try? data.write(to: fileURL!)
} catch {
break
}
if let dir = fileDir {
let sharedurl = dir.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
let furl: URL = URL(string: sharedurl)!
UIApplication.shared.open(furl, options: [:], completionHandler: nil)
}
}
default:
break
}
case 5:
let alert = UIAlertController(title: "Warning", message: "This operation will reset your configuration and generate new keys. This is not reversible unless your configuration has been exported. Changes will not take effect until the next time Yggdrasil is restarted.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Reset", style: .destructive, handler: { action in
self.app.yggdrasilConfig = ConfigurationProxy()
if let config = self.app.yggdrasilConfig {
try? config.save(to: &self.app.vpnManager)
self.viewDidLoad()
}}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
default:
break
}
tableView.deselectRow(at: indexPath, animated: true)
}
@objc func cancelDocumentBrowser() {
self.dismiss(animated: true, completion: nil)
}
@available(iOS 11.0, *)
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) {
do {
if let url = documentURLs.first {
let data = try Data(contentsOf: url)
let conf = try ConfigurationProxy(json: data)
try conf.save(to: &self.app.vpnManager)
self.app.yggdrasilConfig = conf
controller.dismiss(animated: true, completion: nil)
let alert = UIAlertController(title: "Import Configuration", message: "Configuration file has been imported.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
}
} catch {
controller.dismiss(animated: true, completion: nil)
let alert = UIAlertController(title: "Import Failed", message: "Unable to import this configuration file.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
}
self.viewDidLoad()
}
}

View file

@ -0,0 +1,18 @@
//
// SplitViewController.swift
// YggdrasilNetwork
//
// Created by Neil Alexander on 02/01/2019.
//
import UIKit
class SplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
}

View file

@ -0,0 +1,133 @@
import UIKit
import NetworkExtension
import Yggdrasil
class TableViewController: UITableViewController {
var app = UIApplication.shared.delegate as! AppDelegate
@IBOutlet var connectedStatusLabel: UILabel!
@IBOutlet var toggleTableView: UITableView!
@IBOutlet var toggleLabel: UILabel!
@IBOutlet var toggleConnect: UISwitch!
@IBOutlet weak var statsSelfIPCell: UITableViewCell!
@IBOutlet weak var statsSelfSubnetCell: UITableViewCell!
@IBOutlet weak var statsSelfCoordsCell: UITableViewCell!
@IBOutlet var statsSelfIP: UILabel!
@IBOutlet var statsSelfSubnet: UILabel!
@IBOutlet var statsSelfCoords: UILabel!
@IBOutlet var statsSelfPeers: UILabel!
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilSelfUpdated), name: NSNotification.Name.YggdrasilSelfUpdated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilPeersUpdated), name: NSNotification.Name.YggdrasilPeersUpdated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilSwitchPeersUpdated), name: NSNotification.Name.YggdrasilSwitchPeersUpdated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilSettingsUpdated), name: NSNotification.Name.YggdrasilSettingsUpdated, object: nil)
}
@IBAction func onRefreshButton(_ sender: UIButton) {
sender.isEnabled = false
app.makeIPCRequests()
sender.isEnabled = true
}
override func viewWillAppear(_ animated: Bool) {
//NotificationCenter.default.addObserver(self, selector: #selector(TableViewController.VPNStatusDidChange(_:)), name: NSNotification.Name.NEVPNStatusDidChange, object: nil)
if let row = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: row, animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
//NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NEVPNStatusDidChange, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
self.onYggdrasilSelfUpdated(notification: NSNotification.init(name: NSNotification.Name.YggdrasilSettingsUpdated, object: nil))
}
override func viewWillLayoutSubviews() {
self.onYggdrasilSelfUpdated(notification: NSNotification.init(name: NSNotification.Name.YggdrasilSettingsUpdated, object: nil))
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let row = self.tableView.indexPathForSelectedRow {
self.tableView.deselectRow(at: row, animated: true)
}
}
@objc func onYggdrasilSettingsUpdated(notification: NSNotification) {
toggleLabel.isEnabled = !app.vpnManager.isOnDemandEnabled
toggleConnect.isEnabled = !app.vpnManager.isOnDemandEnabled
if let footer = toggleTableView.footerView(forSection: 0) {
if let label = footer.textLabel {
label.text = app.vpnManager.isOnDemandEnabled ? "Yggdrasil will automatically stop and start based on settings." : "You must restart Yggdrasil to make configuration changes effective."
}
}
}
func updateConnectedStatus() {
if self.app.vpnManager.connection.status == .connected {
if app.yggdrasilSwitchPeers.count > 0 {
connectedStatusLabel.text = "Connected"
connectedStatusLabel.textColor = UIColor(red: 0.37, green: 0.79, blue: 0.35, alpha: 1.0)
} else {
connectedStatusLabel.text = "No active connections"
connectedStatusLabel.textColor = UIColor.red
}
} else {
connectedStatusLabel.text = "Not enabled"
connectedStatusLabel.textColor = UIColor.systemGray
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@objc func onYggdrasilSelfUpdated(notification: NSNotification) {
statsSelfIP.text = app.yggdrasilSelfIP
statsSelfSubnet.text = app.yggdrasilSelfSubnet
statsSelfCoords.text = app.yggdrasilSelfCoords
statsSelfIPCell.layoutSubviews()
statsSelfSubnetCell.layoutSubviews()
statsSelfCoordsCell.layoutSubviews()
let status = self.app.vpnManager.connection.status
toggleConnect.isOn = status == .connecting || status == .connected
self.updateConnectedStatus()
}
@objc func onYggdrasilSwitchPeersUpdated(notification: NSNotification) {
self.updateConnectedStatus()
}
@objc func onYggdrasilPeersUpdated(notification: NSNotification) {
let peercount = app.yggdrasilSwitchPeers.count
if peercount <= 0 {
statsSelfPeers.text = "No peers"
} else if peercount == 1 {
statsSelfPeers.text = "\(peercount) peer"
} else {
statsSelfPeers.text = "\(peercount) peers"
}
}
@IBAction func toggleVPNStatus(_ sender: UISwitch, forEvent event: UIEvent) {
if sender.isOn {
do {
try self.app.vpnManager.connection.startVPNTunnel()
} catch {
print(error)
}
} else {
self.app.vpnManager.connection.stopVPNTunnel()
}
}
}