├── .gitignore ├── AppProxy ├── AppProxy.entitlements ├── AppProxyProvider.swift ├── ClientAppProxyConnection.swift └── Info.plist ├── FilterControlProvider ├── ControlExtension.swift ├── ControlFilterExtension.entitlements └── Info.plist ├── FilterDataProvider ├── DataExtension.swift ├── DataFilterExtension.entitlements └── Info.plist ├── LICENSE.txt ├── PacketTunnel ├── Info.plist ├── PacketTunnel.entitlements └── PacketTunnelProvider.swift ├── README.md ├── SimpleTunnel.xcodeproj └── project.pbxproj ├── SimpleTunnel ├── AddEditConfiguration.swift ├── AppDelegate.swift ├── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── ClientTunnelConnection.swift ├── ConfigurationListController.swift ├── ConfigurationParametersViewController.swift ├── ConnectionRuleAddEditController.swift ├── ConnectionRuleListController.swift ├── ContentFilterController.swift ├── EnumPickerController.swift ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── GrayDot.imageset │ │ ├── Contents.json │ │ ├── GrayDot.png │ │ ├── GrayDot@1x.png │ │ └── GrayDot@3x.png │ ├── GreenDot.imageset │ │ ├── Contents.json │ │ ├── GreenDot.png │ │ ├── GreenDot@1x.png │ │ └── GreenDot@3x.png │ └── RedDot.imageset │ │ ├── Contents.json │ │ ├── RedDot.png │ │ ├── RedDot@1x.png │ │ └── RedDot@3x.png ├── Info.plist ├── ListViewController.swift ├── OnDemandRuleAddEditController.swift ├── OnDemandRuleListController.swift ├── ProxyAutoConfigScriptController.swift ├── ProxyServerAddEditController.swift ├── ProxySettingsController.swift ├── SimpleTunnel.entitlements ├── StatusViewController.swift ├── StringListController.swift ├── SwitchCell.swift └── TextFieldCell.swift ├── SimpleTunnelServices ├── ClientTunnel.swift ├── Connection.swift ├── FilterUtilities.swift ├── Info.plist ├── SimpleTunnelServices.h ├── Tunnel.swift └── util.swift └── tunnel_server ├── AddressPool.swift ├── ServerConfiguration.swift ├── ServerConnection.swift ├── ServerTunnel.swift ├── ServerTunnelConnection.swift ├── ServerUtils.h ├── ServerUtils.m ├── UDPServerConnection.swift ├── config.plist ├── main.swift └── tunnel_server-Bridging-Header.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /AppProxy/AppProxy.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | com.apple.managed.vpn.shared 8 | 9 | com.apple.developer.networking.networkextension 10 | 11 | packet-tunnel-provider 12 | app-proxy-provider 13 | content-filter-provider 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /AppProxy/AppProxyProvider.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the AppProxyProvider class. The AppProxyProvider class is a sub-class of NEAppProxyProvider, and is the integration point between the Network Extension framework and the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import NetworkExtension 10 | import SimpleTunnelServices 11 | 12 | /// A NEAppProxyProvider sub-class that implements the client side of the SimpleTunnel tunneling protocol. 13 | class AppProxyProvider: NEAppProxyProvider, TunnelDelegate { 14 | 15 | // MARK: Properties 16 | 17 | /// A reference to the tunnel object. 18 | var tunnel: ClientTunnel? 19 | 20 | /// The completion handler to call when the tunnel is fully established. 21 | var pendingStartCompletion: ((NSError?) -> Void)? 22 | 23 | /// The completion handler to call when the tunnel is fully disconnected. 24 | var pendingStopCompletion: ((Void) -> Void)? 25 | 26 | // MARK: NEAppProxyProvider 27 | 28 | /// Begin the process of establishing the tunnel. 29 | override func startProxy(options: [String : Any]?, completionHandler: @escaping (Error?) -> Void) { 30 | 31 | let newTunnel = ClientTunnel() 32 | newTunnel.delegate = self 33 | 34 | if let error = newTunnel.startTunnel(self) { 35 | completionHandler(error as NSError) 36 | return 37 | } 38 | 39 | pendingStartCompletion = completionHandler 40 | tunnel = newTunnel 41 | } 42 | 43 | /// Begin the process of stopping the tunnel. 44 | override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { 45 | 46 | // Clear out any pending start completion handler. 47 | pendingStartCompletion = nil 48 | 49 | pendingStopCompletion = completionHandler 50 | tunnel?.closeTunnel() 51 | } 52 | 53 | /// Handle a new flow of network data created by an application. 54 | override func handleNewFlow(_ flow: (NEAppProxyFlow?)) -> Bool { 55 | var newConnection: ClientAppProxyConnection? 56 | 57 | guard let clientTunnel = tunnel else { return false } 58 | 59 | if let TCPFlow = flow as? NEAppProxyTCPFlow { 60 | newConnection = ClientAppProxyTCPConnection(tunnel: clientTunnel, newTCPFlow: TCPFlow) 61 | } 62 | else if let UDPFlow = flow as? NEAppProxyUDPFlow { 63 | newConnection = ClientAppProxyUDPConnection(tunnel: clientTunnel, newUDPFlow: UDPFlow) 64 | } 65 | 66 | guard newConnection != nil else { return false } 67 | 68 | newConnection!.open() 69 | 70 | return true 71 | } 72 | 73 | // MARK: TunnelDelegate 74 | 75 | /// Handle the event of the tunnel being fully established. 76 | func tunnelDidOpen(_ targetTunnel: Tunnel) { 77 | guard let clientTunnel = targetTunnel as? ClientTunnel else { 78 | pendingStartCompletion?(SimpleTunnelError.internalError as NSError) 79 | pendingStartCompletion = nil 80 | return 81 | } 82 | simpleTunnelLog("Tunnel opened, fetching configuration") 83 | clientTunnel.sendFetchConfiguation() 84 | } 85 | 86 | /// Handle the event of the tunnel being fully disconnected. 87 | func tunnelDidClose(_ targetTunnel: Tunnel) { 88 | 89 | // Call the appropriate completion handler depending on the current pending tunnel operation. 90 | if pendingStartCompletion != nil { 91 | pendingStartCompletion?(tunnel?.lastError) 92 | pendingStartCompletion = nil 93 | } 94 | else if pendingStopCompletion != nil { 95 | pendingStopCompletion?() 96 | pendingStopCompletion = nil 97 | } 98 | else { 99 | // No completion handler, so cancel the proxy. 100 | cancelProxyWithError(tunnel?.lastError) 101 | } 102 | tunnel = nil 103 | } 104 | 105 | /// Handle the server sending a configuration. 106 | func tunnelDidSendConfiguration(_ targetTunnel: Tunnel, configuration: [String : AnyObject]) { 107 | simpleTunnelLog("Server sent configuration: \(configuration)") 108 | 109 | guard let tunnelAddress = tunnel?.remoteHost else { 110 | let error = SimpleTunnelError.badConnection 111 | pendingStartCompletion?(error as NSError) 112 | pendingStartCompletion = nil 113 | return 114 | } 115 | 116 | guard let DNSDictionary = configuration[SettingsKey.DNS.rawValue] as? [String: AnyObject], let DNSServers = DNSDictionary[SettingsKey.Servers.rawValue] as? [String] else { 117 | self.pendingStartCompletion?(nil) 118 | self.pendingStartCompletion = nil 119 | return 120 | } 121 | 122 | let newSettings = NETunnelNetworkSettings(tunnelRemoteAddress: tunnelAddress) 123 | 124 | newSettings.dnsSettings = NEDNSSettings(servers: DNSServers) 125 | if let DNSSearchDomains = DNSDictionary[SettingsKey.SearchDomains.rawValue] as? [String] { 126 | newSettings.dnsSettings?.searchDomains = DNSSearchDomains 127 | } 128 | 129 | simpleTunnelLog("Calling setTunnelNetworkSettings") 130 | 131 | self.setTunnelNetworkSettings(newSettings) { error in 132 | if error != nil { 133 | let startError = SimpleTunnelError.badConfiguration 134 | self.pendingStartCompletion?(startError as NSError) 135 | self.pendingStartCompletion = nil 136 | } 137 | else { 138 | self.pendingStartCompletion?(nil) 139 | self.pendingStartCompletion = nil 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /AppProxy/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | AppProxy 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.networkextension.app-proxy 29 | NSExtensionPrincipalClass 30 | $(PRODUCT_MODULE_NAME).AppProxyProvider 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /FilterControlProvider/ControlExtension.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ControlExtension class. The ControlExtension class is a sub-class of NEFilterControlProvider, and is responsible for downloading content filter rules from a web service. 7 | */ 8 | 9 | import NetworkExtension 10 | import Foundation 11 | import SimpleTunnelServices 12 | 13 | /// A NEFitlerControlProvider sub-class that implements logic for downloading rules from a web server. 14 | class ControlExtension : NEFilterControlProvider { 15 | 16 | // MARK: Properties 17 | 18 | /// The default rules, in the event that 19 | let defaultRules: [String: [String: AnyObject]] = [ 20 | "www.apple.com" : [ 21 | "kRule" : FilterRuleAction.block.rawValue as AnyObject, 22 | "kRemediationKey" : "Remediate1" as AnyObject 23 | ] 24 | ] 25 | 26 | /// An integer to use as the context for key-value observing. 27 | var observerContext = 0 28 | 29 | // MARK: Interface 30 | 31 | /// Update the filter based on changes to the configuration 32 | func updateFromConfiguration() { 33 | guard let serverAddress = filterConfiguration.serverAddress else { return } 34 | 35 | FilterUtilities.defaults?.setValue(defaultRules, forKey: "rules") 36 | FilterUtilities.fetchRulesFromServer(filterConfiguration.serverAddress) 37 | 38 | let remediationURL = "https://\(serverAddress)/remediate/?url=\(NEFilterProviderRemediationURLFlowURLHostname)&organization=\(NEFilterProviderRemediationURLOrganization)&username=\(NEFilterProviderRemediationURLUsername)" 39 | 40 | simpleTunnelLog("Remediation url is \(remediationURL)") 41 | 42 | remediationMap = 43 | [ 44 | NEFilterProviderRemediationMapRemediationURLs : [ "Remediate1" : remediationURL as NSObject ], 45 | NEFilterProviderRemediationMapRemediationButtonTexts : 46 | [ 47 | "RemediateButton1" : "Request Access" as NSObject, 48 | "RemediateButton2" : "\"" as NSObject, 49 | "RemediateButton3" : "Request Access 3" as NSObject, 50 | ] 51 | ] 52 | 53 | self.urlAppendStringMap = [ "SafeYes" : "safe=yes", "Adult" : "adult=yes"] 54 | 55 | simpleTunnelLog("Remediation map set") 56 | } 57 | 58 | // MARK: Initializers 59 | 60 | override init() { 61 | super.init() 62 | updateFromConfiguration() 63 | 64 | FilterUtilities.defaults?.setValue(defaultRules, forKey: "rules") 65 | FilterUtilities.fetchRulesFromServer(self.filterConfiguration.serverAddress) 66 | 67 | self.addObserver(self, forKeyPath: "filterConfiguration", options: [.initial, .new], context: &observerContext) 68 | } 69 | 70 | // MARK: NSObject 71 | 72 | /// Observe changes to the configuration. 73 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 74 | if keyPath == "filterConfiguration" && context == &observerContext { 75 | simpleTunnelLog("configuration changed") 76 | updateFromConfiguration() 77 | } else { 78 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 79 | } 80 | } 81 | 82 | // MARK: NEFilterControlProvider 83 | 84 | /// Handle a new flow of network data 85 | override func handleNewFlow(_ flow: NEFilterFlow, completionHandler: @escaping (NEFilterControlVerdict) -> Void) { 86 | simpleTunnelLog("Handle new flow called") 87 | var controlVerdict = NEFilterControlVerdict.updateRules() 88 | let (ruleType, hostname, _) = FilterUtilities.getRule(flow) 89 | 90 | switch ruleType { 91 | case .needMoreRulesAndAllow: 92 | simpleTunnelLog("\(hostname) is set to be Allowed") 93 | controlVerdict = NEFilterControlVerdict.allow(withUpdateRules: false) 94 | 95 | case .needMoreRulesAndBlock: 96 | simpleTunnelLog("\(hostname) is set to be blocked") 97 | controlVerdict = NEFilterControlVerdict.drop(withUpdateRules: false) 98 | 99 | default: 100 | simpleTunnelLog("\(hostname) is not set for need more rules") 101 | } 102 | 103 | completionHandler(controlVerdict) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /FilterControlProvider/ControlFilterExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | com.apple.managed.vpn.shared 8 | 9 | com.apple.developer.networking.networkextension 10 | 11 | packet-tunnel-provider 12 | app-proxy-provider 13 | content-filter-provider 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FilterControlProvider/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | FilterControlProvider 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.networkextension.filter-control 29 | NSExtensionPrincipalClass 30 | FilterControlProvider.ControlExtension 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /FilterDataProvider/DataFilterExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | keychain-access-groups 6 | 7 | com.apple.managed.vpn.shared 8 | 9 | com.apple.developer.networking.networkextension 10 | 11 | packet-tunnel-provider 12 | app-proxy-provider 13 | content-filter-provider 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FilterDataProvider/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | FilterDataProvider 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.networkextension.filter-data 29 | NSExtensionPrincipalClass 30 | FilterDataProvider.DataExtension 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Sample code project: SimpleTunnel: Customized Networking Using the NetworkExtension Framework 2 | Version: 1.3 3 | 4 | IMPORTANT: This Apple software is supplied to you by Apple 5 | Inc. ("Apple") in consideration of your agreement to the following 6 | terms, and your use, installation, modification or redistribution of 7 | this Apple software constitutes acceptance of these terms. If you do 8 | not agree with these terms, please do not use, install, modify or 9 | redistribute this Apple software. 10 | 11 | In consideration of your agreement to abide by the following terms, and 12 | subject to these terms, Apple grants you a personal, non-exclusive 13 | license, under Apple's copyrights in this original Apple software (the 14 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 15 | Software, with or without modifications, in source and/or binary forms; 16 | provided that if you redistribute the Apple Software in its entirety and 17 | without modifications, you must retain this notice and the following 18 | text and disclaimers in all such redistributions of the Apple Software. 19 | Neither the name, trademarks, service marks or logos of Apple Inc. may 20 | be used to endorse or promote products derived from the Apple Software 21 | without specific prior written permission from Apple. Except as 22 | expressly stated in this notice, no other rights or licenses, express or 23 | implied, are granted by Apple herein, including but not limited to any 24 | patent rights that may be infringed by your derivative works or by other 25 | works in which the Apple Software may be incorporated. 26 | 27 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 28 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 29 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 30 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 31 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 32 | 33 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 34 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 35 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 36 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 37 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 38 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 39 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 40 | POSSIBILITY OF SUCH DAMAGE. 41 | 42 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 43 | -------------------------------------------------------------------------------- /PacketTunnel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | PacketTunnel 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | XPC! 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSExtension 26 | 27 | NSExtensionPointIdentifier 28 | com.apple.networkextension.packet-tunnel 29 | NSExtensionPrincipalClass 30 | $(PRODUCT_MODULE_NAME).PacketTunnelProvider 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /PacketTunnel/PacketTunnel.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | packet-tunnel-provider 8 | app-proxy-provider 9 | content-filter-provider 10 | 11 | keychain-access-groups 12 | 13 | $(AppIdentifierPrefix)com.telekom.SimpleTunnel 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PacketTunnel/PacketTunnelProvider.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the PacketTunnelProvider class. The PacketTunnelProvider class is a sub-class of NEPacketTunnelProvider, and is the integration point between the Network Extension framework and the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import NetworkExtension 10 | import SimpleTunnelServices 11 | 12 | /// A packet tunnel provider object. 13 | class PacketTunnelProvider: NEPacketTunnelProvider, TunnelDelegate, ClientTunnelConnectionDelegate { 14 | 15 | // MARK: Properties 16 | 17 | /// A reference to the tunnel object. 18 | var tunnel: ClientTunnel? 19 | 20 | /// The single logical flow of packets through the tunnel. 21 | var tunnelConnection: ClientTunnelConnection? 22 | 23 | /// The completion handler to call when the tunnel is fully established. 24 | var pendingStartCompletion: ((Error?) -> Void)? 25 | 26 | /// The completion handler to call when the tunnel is fully disconnected. 27 | var pendingStopCompletion: ((Void) -> Void)? 28 | 29 | // MARK: NEPacketTunnelProvider 30 | 31 | /// Begin the process of establishing the tunnel. 32 | override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { 33 | let newTunnel = ClientTunnel() 34 | newTunnel.delegate = self 35 | 36 | if let error = newTunnel.startTunnel(self) { 37 | completionHandler(error as NSError) 38 | } 39 | else { 40 | // Save the completion handler for when the tunnel is fully established. 41 | pendingStartCompletion = completionHandler 42 | tunnel = newTunnel 43 | } 44 | } 45 | 46 | /// Begin the process of stopping the tunnel. 47 | override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { 48 | // Clear out any pending start completion handler. 49 | pendingStartCompletion = nil 50 | 51 | // Save the completion handler for when the tunnel is fully disconnected. 52 | pendingStopCompletion = completionHandler 53 | tunnel?.closeTunnel() 54 | } 55 | 56 | /// Handle IPC messages from the app. 57 | override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { 58 | guard let messageString = NSString(data: messageData, encoding: String.Encoding.utf8.rawValue) else { 59 | completionHandler?(nil) 60 | return 61 | } 62 | 63 | simpleTunnelLog("Got a message from the app: \(messageString)") 64 | 65 | let responseData = "Hello app".data(using: String.Encoding.utf8) 66 | completionHandler?(responseData) 67 | } 68 | 69 | // MARK: TunnelDelegate 70 | 71 | /// Handle the event of the tunnel connection being established. 72 | func tunnelDidOpen(_ targetTunnel: Tunnel) { 73 | // Open the logical flow of packets through the tunnel. 74 | let newConnection = ClientTunnelConnection(tunnel: tunnel!, clientPacketFlow: packetFlow, connectionDelegate: self) 75 | newConnection.open() 76 | tunnelConnection = newConnection 77 | } 78 | 79 | /// Handle the event of the tunnel connection being closed. 80 | func tunnelDidClose(_ targetTunnel: Tunnel) { 81 | if pendingStartCompletion != nil { 82 | // Closed while starting, call the start completion handler with the appropriate error. 83 | pendingStartCompletion?(tunnel?.lastError) 84 | pendingStartCompletion = nil 85 | } 86 | else if pendingStopCompletion != nil { 87 | // Closed as the result of a call to stopTunnelWithReason, call the stop completion handler. 88 | pendingStopCompletion?() 89 | pendingStopCompletion = nil 90 | } 91 | else { 92 | // Closed as the result of an error on the tunnel connection, cancel the tunnel. 93 | cancelTunnelWithError(tunnel?.lastError) 94 | } 95 | tunnel = nil 96 | } 97 | 98 | /// Handle the server sending a configuration. 99 | func tunnelDidSendConfiguration(_ targetTunnel: Tunnel, configuration: [String : AnyObject]) { 100 | } 101 | 102 | // MARK: ClientTunnelConnectionDelegate 103 | 104 | /// Handle the event of the logical flow of packets being established through the tunnel. 105 | func tunnelConnectionDidOpen(_ connection: ClientTunnelConnection, configuration: [NSObject: AnyObject]) { 106 | 107 | // Create the virtual interface settings. 108 | guard let settings = createTunnelSettingsFromConfiguration(configuration) else { 109 | pendingStartCompletion?(SimpleTunnelError.internalError as NSError) 110 | pendingStartCompletion = nil 111 | return 112 | } 113 | 114 | // Set the virtual interface settings. 115 | setTunnelNetworkSettings(settings) { error in 116 | var startError: NSError? 117 | if let error = error { 118 | simpleTunnelLog("Failed to set the tunnel network settings: \(error)") 119 | startError = SimpleTunnelError.badConfiguration as NSError 120 | } 121 | else { 122 | // Now we can start reading and writing packets to/from the virtual interface. 123 | self.tunnelConnection?.startHandlingPackets() 124 | } 125 | 126 | // Now the tunnel is fully established, call the start completion handler. 127 | self.pendingStartCompletion?(startError) 128 | self.pendingStartCompletion = nil 129 | } 130 | } 131 | 132 | /// Handle the event of the logical flow of packets being torn down. 133 | func tunnelConnectionDidClose(_ connection: ClientTunnelConnection, error: NSError?) { 134 | tunnelConnection = nil 135 | tunnel?.closeTunnelWithError(error) 136 | } 137 | 138 | /// Create the tunnel network settings to be applied to the virtual interface. 139 | func createTunnelSettingsFromConfiguration(_ configuration: [NSObject: AnyObject]) -> NEPacketTunnelNetworkSettings? { 140 | guard let tunnelAddress = tunnel?.remoteHost, 141 | let address = getValueFromPlist(configuration, keyArray: [.IPv4, .Address]) as? String, 142 | let netmask = getValueFromPlist(configuration, keyArray: [.IPv4, .Netmask]) as? String 143 | else { return nil } 144 | 145 | let newSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelAddress) 146 | var fullTunnel = true 147 | 148 | newSettings.iPv4Settings = NEIPv4Settings(addresses: [address], subnetMasks: [netmask]) 149 | 150 | if let routes = getValueFromPlist(configuration, keyArray: [.IPv4, .Routes]) as? [[String: AnyObject]] { 151 | var includedRoutes = [NEIPv4Route]() 152 | for route in routes { 153 | if let netAddress = route[SettingsKey.Address.rawValue] as? String, 154 | let netMask = route[SettingsKey.Netmask.rawValue] as? String 155 | { 156 | includedRoutes.append(NEIPv4Route(destinationAddress: netAddress, subnetMask: netMask)) 157 | } 158 | } 159 | newSettings.iPv4Settings?.includedRoutes = includedRoutes 160 | fullTunnel = false 161 | } 162 | else { 163 | // No routes specified, use the default route. 164 | newSettings.iPv4Settings?.includedRoutes = [NEIPv4Route.default()] 165 | } 166 | 167 | if let DNSDictionary = configuration[SettingsKey.DNS.rawValue as NSString] as? [String: AnyObject], 168 | let DNSServers = DNSDictionary[SettingsKey.Servers.rawValue] as? [String] 169 | { 170 | newSettings.dnsSettings = NEDNSSettings(servers: DNSServers) 171 | if let DNSSearchDomains = DNSDictionary[SettingsKey.SearchDomains.rawValue] as? [String] { 172 | newSettings.dnsSettings?.searchDomains = DNSSearchDomains 173 | if !fullTunnel { 174 | newSettings.dnsSettings?.matchDomains = DNSSearchDomains 175 | } 176 | } 177 | } 178 | 179 | newSettings.tunnelOverheadBytes = 150 180 | 181 | return newSettings 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleTunnel: Customized Networking Using the NetworkExtension Framework 2 | 3 | The SimpleTunnel project contains working examples of the four extension points provided by the Network Extension framework: 4 | 5 | 1. Packet Tunnel Provider 6 | 7 | The Packet Tunnel Provider extension point is used to implemnt the client side of a custom network tunneling protocol that encapsulates network data in the form of IP packets. The PacketTunnel target in the SimpleTunnel project produces a sample Packet Tunnel Provider extension. 8 | 9 | 2. App Proxy Provider 10 | 11 | The App Proxy Provider extension point is used to implement the client side of a custom network proxy protocol that encapsulates network data in the form of flows of application network data. Both TCP or stream-based and UDP or datagram-based flows of data are supported. The AppProxy target in the SimpleTunnel project produces a sample App Proxy Provider extension. 12 | 13 | 3. Filter Data Provider and Filter Control Provider 14 | 15 | The two Filter Provider extension points are used to implement a dynamic, on-device network content filter. The Filter Data Provider extension is responsible for examining network data and making pass/block decisions. The Filter Data Provider extension sandbox prevents the extension from communicating over the network or writing to disk to prevent any leakage of network data. The Filter Control Provider extension can communicate using the network and write to the disk. It is responsible for updating the filtering rules on behalf of the Filter Data Provider extension. 16 | 17 | The FilterDataProvider target in the SimpleTunnel project produces a sample Filter Data Provider extension. The FilterControlProvider target in the SimpleTunnel project produces a sample Filter Control Provider extension.e 18 | 19 | All of the sample extensions are packaged into the SimpleTunnel app. The SimpleTunnel app contains code demonstrating how to configure and control the various types of Network Extension providers. The SimpleTunnel target in the SimpleTunnel project produces the SimpleTunnel app and all of the sample extensions. 20 | 21 | The SimpleTunnel project contains both the client and server sides of a custom network tunneling protocol. The Packet Tunnel Provider and App Proxy Provider extensions implement the client side. The tunnel_server target produces a OS X command-line binary that implements the server side. The server is configured using a plist. A sample plist is included in the tunnel_server source. To run the server, use this command: 22 | 23 | sudo tunnel_server 24 | 25 | # Requirements 26 | 27 | ### Runtime 28 | 29 | The NEProvider family of APIs require the following entitlement: 30 | 31 | com.apple.developer.networking.networkextension 32 | 33 | packet-tunnel-provider 34 | app-proxy-provider 35 | content-filter-provider 36 | 37 | 38 | 39 | The SimpleTunnel.app and the provider extensions will not run if they are not code signed with this entitlement. 40 | 41 | You can request this entitlement by sending an email to networkextension@apple.com. 42 | 43 | The SimpleTunnel iOS products require iOS 9.0 or newer. 44 | The SimpleTunnel OS X products require OS X 11.0 or newer. 45 | 46 | ### Build 47 | 48 | SimpleTunnel requires Xcode 8.0 or newer. 49 | The SimpleTunnel iOS targets require the iOS 9.0 SDK or newer. 50 | The SimpleTunnel OS X targets require the OS X 11.0 SDK or newer. 51 | 52 | Copyright (C) 2016 Apple Inc. All rights reserved. 53 | -------------------------------------------------------------------------------- /SimpleTunnel/AddEditConfiguration.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the AddEditConfiguration class, which is responsible for controlling a view used to create or edit a VPN configuration. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | import Security 12 | import SimpleTunnelServices 13 | 14 | /// A view controller object for a table view containing input fields used to specify configuration parameters for a VPN configuration. 15 | class AddEditConfiguration: ConfigurationParametersViewController { 16 | 17 | // MARK: Properties 18 | 19 | /// A table view cell containing the text field where the name of the configuration is entered. 20 | @IBOutlet weak var nameCell: TextFieldCell! 21 | 22 | /// A table view cell containing the text field where the server address of the configuration is entered. 23 | @IBOutlet weak var serverAddressCell: TextFieldCell! 24 | 25 | /// A table view cell containing the text field where the username of the configuration is entered. 26 | @IBOutlet weak var usernameCell: TextFieldCell! 27 | 28 | /// A table view cell containing the text field where the password of the configuration is entered. 29 | @IBOutlet weak var passwordCell: TextFieldCell! 30 | 31 | /// A table view cell containing a switch used to enable and disable Connect On Demand for the configuration. 32 | @IBOutlet weak var onDemandCell: SwitchCell! 33 | 34 | /// A table view cell containing a switch used to enable and disable proxy settings for the configuration. 35 | @IBOutlet weak var proxiesCell: SwitchCell! 36 | 37 | /// A table view cell containing a switch used to enable and disable Disconnect On Sleep for the configuration. 38 | @IBOutlet weak var disconnectOnSleepCell: SwitchCell! 39 | 40 | /// A table view cell that when tapped transitions the app to a view where the Connect On Demand rules are managed. 41 | @IBOutlet weak var onDemandRulesCell: UITableViewCell! 42 | 43 | /// A table view cell that when tapped transitions the app to a view where the proxy settings are managed. 44 | @IBOutlet weak var proxySettingsCell: UITableViewCell! 45 | 46 | /// The NEVPNManager object corresponding to the configuration being added or edited. 47 | var targetManager: NEVPNManager = NEVPNManager.shared() 48 | 49 | // MARK: UIViewController 50 | 51 | /// Handle the event of the view being loaded into memory. 52 | override func viewDidLoad() { 53 | super.viewDidLoad() 54 | 55 | // Set up the table view cells 56 | 57 | cells = [ 58 | nameCell, 59 | serverAddressCell, 60 | usernameCell, 61 | passwordCell, 62 | onDemandCell, 63 | proxiesCell, 64 | disconnectOnSleepCell 65 | ].flatMap { $0 } 66 | 67 | // The switch in proxiesCell controls the display of proxySettingsCell 68 | proxiesCell.dependentCells = [ proxySettingsCell ] 69 | proxiesCell.getIndexPath = { 70 | return self.getIndexPathOfCell(self.proxiesCell) 71 | } 72 | proxiesCell.valueChanged = { 73 | self.updateCellsWithDependentsOfCell(self.proxiesCell) 74 | } 75 | 76 | // The switch in onDemandCell controls the display of onDemandRulesCell 77 | onDemandCell.dependentCells = [ onDemandRulesCell ] 78 | onDemandCell.getIndexPath = { 79 | return self.getIndexPathOfCell(self.onDemandCell) 80 | } 81 | onDemandCell.valueChanged = { 82 | self.updateCellsWithDependentsOfCell(self.onDemandCell) 83 | self.targetManager.isOnDemandEnabled = self.onDemandCell.isOn 84 | } 85 | 86 | disconnectOnSleepCell.valueChanged = { 87 | self.targetManager.protocolConfiguration?.disconnectOnSleep = self.disconnectOnSleepCell.isOn 88 | } 89 | 90 | nameCell.valueChanged = { 91 | self.targetManager.localizedDescription = self.nameCell.textField.text 92 | } 93 | 94 | serverAddressCell.valueChanged = { 95 | self.targetManager.protocolConfiguration?.serverAddress = self.serverAddressCell.textField.text 96 | } 97 | 98 | usernameCell.valueChanged = { 99 | self.targetManager.protocolConfiguration?.username = self.usernameCell.textField.text 100 | } 101 | } 102 | 103 | /// Handle the event of the view being displayed. 104 | override func viewWillAppear(_ animated: Bool) { 105 | super.viewWillAppear(animated) 106 | 107 | tableView.reloadData() 108 | 109 | // Set the text fields and switches per the settings in the configuration. 110 | 111 | nameCell.textField.text = targetManager.localizedDescription 112 | serverAddressCell.textField.text = targetManager.protocolConfiguration?.serverAddress 113 | usernameCell.textField.text = targetManager.protocolConfiguration?.username 114 | 115 | if let passRef = targetManager.protocolConfiguration?.passwordReference { 116 | passwordCell.textField.text = getPasswordWithPersistentReference(passRef) 117 | } 118 | else { 119 | passwordCell.textField.text = nil 120 | } 121 | 122 | disconnectOnSleepCell.isOn = targetManager.protocolConfiguration?.disconnectOnSleep ?? false 123 | 124 | onDemandCell.isOn = targetManager.isOnDemandEnabled 125 | 126 | onDemandRulesCell.detailTextLabel?.text = getDescriptionForListValue(targetManager.onDemandRules, itemDescription: "rule") 127 | 128 | proxiesCell.isOn = targetManager.protocolConfiguration?.proxySettings != nil 129 | } 130 | 131 | /// Set up the destination view controller of a segue away from this view controller. 132 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 133 | guard let identifier = segue.identifier else { return } 134 | 135 | switch identifier { 136 | case "edit-proxy-settings": 137 | // The user tapped on the proxy settings cell. 138 | guard let controller = segue.destination as? ProxySettingsController else { break } 139 | if targetManager.protocolConfiguration?.proxySettings == nil { 140 | targetManager.protocolConfiguration?.proxySettings = NEProxySettings() 141 | } 142 | controller.targetConfiguration = targetManager.protocolConfiguration ?? NETunnelProviderProtocol() 143 | 144 | case "edit-on-demand-rules": 145 | // The user tapped on the Connect On Demand rules cell. 146 | guard let controller = segue.destination as? OnDemandRuleListController else { break } 147 | controller.targetManager = targetManager 148 | 149 | default: 150 | break 151 | } 152 | } 153 | 154 | // MARK: Interface 155 | 156 | /// Set the target configuration and the title to display in the view. 157 | func setTargetManager(_ manager: NEVPNManager?, title: String?) { 158 | if let newManager = manager { 159 | // A manager was given, so an existing configuration is being edited. 160 | targetManager = newManager 161 | } 162 | else { 163 | // No manager was given, create a new configuration. 164 | let newManager = NETunnelProviderManager() 165 | newManager.protocolConfiguration = NETunnelProviderProtocol() 166 | newManager.localizedDescription = "Demo VPN" 167 | newManager.protocolConfiguration?.serverAddress = "TunnelServer" 168 | targetManager = newManager 169 | } 170 | navigationItem.title = title 171 | } 172 | 173 | /// Save the configuration to the Network Extension preferences. 174 | @IBAction func saveTargetManager(_ sender: AnyObject) { 175 | if !proxiesCell.isOn { 176 | targetManager.protocolConfiguration?.proxySettings = nil 177 | } 178 | targetManager.saveToPreferences { error in 179 | if let saveError = error { 180 | simpleTunnelLog("Failed to save the configuration: \(saveError)") 181 | return 182 | } 183 | 184 | // Transition back to the configuration list view. 185 | self.performSegue(withIdentifier: "save-configuration", sender: sender) 186 | } 187 | } 188 | 189 | /// Save a password in the keychain. 190 | func savePassword(_ password: String, inKeychainItem: Data?) -> Data? { 191 | guard let passwordData = password.data(using: String.Encoding.utf8, allowLossyConversion: false) else { return nil } 192 | var status = errSecSuccess 193 | 194 | if let persistentReference = inKeychainItem { 195 | // A persistent reference was given, update the corresponding keychain item. 196 | let query: [NSObject: AnyObject] = [ 197 | kSecValuePersistentRef : persistentReference as AnyObject, 198 | kSecReturnAttributes : kCFBooleanTrue 199 | ] 200 | var result: AnyObject? 201 | 202 | // Get the current attributes for the item. 203 | status = SecItemCopyMatching(query as CFDictionary, &result) 204 | 205 | if let attributes = result as? [NSObject: AnyObject] , status == errSecSuccess { 206 | // Update the attributes with the new data. 207 | var updateQuery = [NSObject: AnyObject]() 208 | updateQuery[kSecClass] = kSecClassGenericPassword 209 | updateQuery[kSecAttrService] = attributes[kSecAttrService] 210 | 211 | var newAttributes = attributes 212 | newAttributes[kSecValueData] = passwordData as AnyObject? 213 | 214 | status = SecItemUpdate(updateQuery as CFDictionary, newAttributes as CFDictionary) 215 | if status == errSecSuccess { 216 | return persistentReference 217 | } 218 | } 219 | } 220 | 221 | if inKeychainItem == nil || status != errSecSuccess { 222 | // No persistent reference was provided, or the update failed. Add a new keychain item. 223 | 224 | let attributes: [NSObject: AnyObject] = [ 225 | kSecAttrService : UUID().uuidString as AnyObject, 226 | kSecValueData : passwordData as AnyObject, 227 | kSecAttrAccessible : kSecAttrAccessibleAlways, 228 | kSecClass : kSecClassGenericPassword, 229 | kSecReturnPersistentRef : kCFBooleanTrue 230 | ] 231 | 232 | var result: AnyObject? 233 | status = SecItemAdd(attributes as CFDictionary, &result) 234 | 235 | if let newPersistentReference = result as? Data , status == errSecSuccess { 236 | return newPersistentReference 237 | } 238 | } 239 | return nil 240 | } 241 | 242 | /// Remove a password from the keychain. 243 | func removePasswordWithPersistentReference(_ persistentReference: Data) { 244 | let query: [NSObject: AnyObject] = [ 245 | kSecClass : kSecClassGenericPassword, 246 | kSecValuePersistentRef : persistentReference as AnyObject 247 | ] 248 | 249 | let status = SecItemDelete(query as CFDictionary) 250 | if status != errSecSuccess { 251 | simpleTunnelLog("Failed to delete a password: \(status)") 252 | } 253 | } 254 | 255 | /// Get a password from the keychain. 256 | func getPasswordWithPersistentReference(_ persistentReference: Data) -> String? { 257 | var result: String? 258 | let query: [NSObject: AnyObject] = [ 259 | kSecClass : kSecClassGenericPassword, 260 | kSecReturnData : kCFBooleanTrue, 261 | kSecValuePersistentRef : persistentReference as AnyObject 262 | ] 263 | 264 | var returnValue: AnyObject? 265 | let status = SecItemCopyMatching(query as CFDictionary, &returnValue) 266 | 267 | if let passwordData = returnValue as? Data , status == errSecSuccess { 268 | result = String(data: passwordData, encoding: String.Encoding.utf8) 269 | } 270 | return result 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /SimpleTunnel/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the UIApplicationDelegate class for the SimpleTunnel app. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// The UIApplicationDelegate for the SimpleTunnel app. 13 | @UIApplicationMain 14 | class AppDelegate: UIResponder, UIApplicationDelegate { 15 | 16 | // MARK: Properties 17 | 18 | /// The main UIWindow object for SimpleTunnel. 19 | var window: UIWindow? 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /SimpleTunnel/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /SimpleTunnel/ClientTunnelConnection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ClientTunnelConnection class. The ClientTunnelConnection class handles the encapsulation and decapsulation of IP packets in the client side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | import SimpleTunnelServices 11 | import NetworkExtension 12 | 13 | // MARK: Protocols 14 | 15 | /// The delegate protocol for ClientTunnelConnection. 16 | protocol ClientTunnelConnectionDelegate { 17 | /// Handle the connection being opened. 18 | func tunnelConnectionDidOpen(_ connection: ClientTunnelConnection, configuration: [NSObject: AnyObject]) 19 | /// Handle the connection being closed. 20 | func tunnelConnectionDidClose(_ connection: ClientTunnelConnection, error: NSError?) 21 | } 22 | 23 | /// An object used to tunnel IP packets using the SimpleTunnel protocol. 24 | class ClientTunnelConnection: Connection { 25 | 26 | // MARK: Properties 27 | 28 | /// The connection delegate. 29 | let delegate: ClientTunnelConnectionDelegate 30 | 31 | /// The flow of IP packets. 32 | let packetFlow: NEPacketTunnelFlow 33 | 34 | // MARK: Initializers 35 | 36 | init(tunnel: ClientTunnel, clientPacketFlow: NEPacketTunnelFlow, connectionDelegate: ClientTunnelConnectionDelegate) { 37 | delegate = connectionDelegate 38 | packetFlow = clientPacketFlow 39 | let newConnectionIdentifier = arc4random() 40 | super.init(connectionIdentifier: Int(newConnectionIdentifier), parentTunnel: tunnel) 41 | } 42 | 43 | // MARK: Interface 44 | 45 | /// Open the connection by sending a "connection open" message to the tunnel server. 46 | func open() { 47 | guard let clientTunnel = tunnel as? ClientTunnel else { return } 48 | 49 | let properties = createMessagePropertiesForConnection(identifier, commandType: .open, extraProperties:[ 50 | TunnelMessageKey.TunnelType.rawValue: TunnelLayer.ip.rawValue as AnyObject 51 | ]) 52 | 53 | clientTunnel.sendMessage(properties) { error in 54 | if let error = error { 55 | self.delegate.tunnelConnectionDidClose(self, error: error) 56 | } 57 | } 58 | } 59 | 60 | /// Handle packets coming from the packet flow. 61 | func handlePackets(_ packets: [Data], protocols: [NSNumber]) { 62 | guard let clientTunnel = tunnel as? ClientTunnel else { return } 63 | 64 | let properties = createMessagePropertiesForConnection(identifier, commandType: .packets, extraProperties:[ 65 | TunnelMessageKey.Packets.rawValue: packets as AnyObject, 66 | TunnelMessageKey.Protocols.rawValue: protocols as AnyObject 67 | ]) 68 | 69 | clientTunnel.sendMessage(properties) { error in 70 | if let sendError = error { 71 | self.delegate.tunnelConnectionDidClose(self, error: sendError) 72 | return 73 | } 74 | 75 | // Read more packets. 76 | self.packetFlow.readPackets { inPackets, inProtocols in 77 | self.handlePackets(inPackets, protocols: inProtocols) 78 | } 79 | } 80 | } 81 | 82 | /// Make the initial readPacketsWithCompletionHandler call. 83 | func startHandlingPackets() { 84 | packetFlow.readPackets { inPackets, inProtocols in 85 | self.handlePackets(inPackets, protocols: inProtocols) 86 | } 87 | } 88 | 89 | // MARK: Connection 90 | 91 | /// Handle the event of the connection being established. 92 | override func handleOpenCompleted(_ resultCode: TunnelConnectionOpenResult, properties: [NSObject: AnyObject]) { 93 | guard resultCode == .success else { 94 | delegate.tunnelConnectionDidClose(self, error: SimpleTunnelError.badConnection as NSError) 95 | return 96 | } 97 | 98 | // Pass the tunnel network settings to the delegate. 99 | if let configuration = properties[TunnelMessageKey.Configuration.rawValue as NSString] as? [NSObject: AnyObject] { 100 | delegate.tunnelConnectionDidOpen(self, configuration: configuration) 101 | } 102 | else { 103 | delegate.tunnelConnectionDidOpen(self, configuration: [:]) 104 | } 105 | } 106 | 107 | /// Send packets to the virtual interface to be injected into the IP stack. 108 | override func sendPackets(_ packets: [Data], protocols: [NSNumber]) { 109 | packetFlow.writePackets(packets, withProtocols: protocols) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /SimpleTunnel/ConfigurationListController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ConfigurationListController class, which is responsible for controlling a list of VPN configurations. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | import SimpleTunnelServices 12 | 13 | /// The view controller object for the list of packet tunnel configurations. 14 | class ConfigurationListController: ListViewController { 15 | 16 | // MARK: Properties 17 | 18 | /// The image to display for configurations that are disabled. 19 | let disabledImage = UIImage(named: "GrayDot") 20 | 21 | /// The image to display for configurations that are enabled but are disconnected. 22 | let disconnectedImage = UIImage(named: "RedDot") 23 | 24 | /// The image to display for configurations that are active (or not disconnected). 25 | let notDisconnectedImage = UIImage(named: "GreenDot") 26 | 27 | /// A list of NEVPNManager objects for the packet tunnel configurations. 28 | var managers = [NEVPNManager]() 29 | 30 | // MARK: UIViewController 31 | 32 | /// Handle the event of the view loading into memory. 33 | override func viewDidLoad() { 34 | isAddEnabled = true 35 | super.viewDidLoad() 36 | } 37 | 38 | /// Handle the event of the view being displayed. 39 | override func viewWillAppear(_ animated: Bool) { 40 | super.viewWillAppear(animated) 41 | 42 | // Re-load all of the configurations. 43 | reloadManagers() 44 | } 45 | 46 | // MARK: Interface 47 | 48 | /// Re-load all of the packet tunnel configurations from the Network Extension preferences 49 | func reloadManagers() { 50 | NETunnelProviderManager.loadAllFromPreferences() { newManagers, error in 51 | guard let vpnManagers = newManagers else { return } 52 | 53 | self.stopObservingStatus() 54 | self.managers = vpnManagers 55 | self.observeStatus() 56 | 57 | // If there are no configurations, automatically go into editing mode. 58 | if self.managers.count == 0 && !self.isEditing { 59 | self.setEditing(true, animated: false) 60 | } 61 | 62 | self.tableView.reloadData() 63 | } 64 | } 65 | 66 | /// Register for configuration change notifications. 67 | func observeStatus() { 68 | for (index, manager) in managers.enumerated() { 69 | NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection, queue: OperationQueue.main, using: { notification in 70 | self.tableView.reloadRows(at: [ IndexPath(row: index, section: 0) ], with: .fade) 71 | }) 72 | } 73 | } 74 | 75 | /// De-register for configuration change notifications. 76 | func stopObservingStatus() { 77 | for manager in managers { 78 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NEVPNStatusDidChange, object: manager.connection) 79 | } 80 | } 81 | 82 | /// Unwind segue handler. 83 | @IBAction func handleUnwind(_ sender: UIStoryboardSegue) { 84 | } 85 | 86 | // MARK: ListViewController 87 | 88 | /// Set up the destination view controller of a segue away from this view controller. 89 | override func listSetupSegue(_ segue: UIStoryboardSegue, forItemAtIndex index: Int) { 90 | 91 | guard let identifier = segue.identifier else { return } 92 | 93 | switch identifier { 94 | case "start-new-configuration": 95 | // The user tapped on the "Add Configuration..." table row. 96 | guard let addEditController = segue.destination as? AddEditConfiguration else { break } 97 | 98 | addEditController.setTargetManager(nil, title: "Add Configuration") 99 | 100 | case "edit-configuration": 101 | // The user tapped on the disclosure button of a configuration while in editing mode. 102 | guard let addEditController = segue.destination as? AddEditConfiguration , index < managers.count else { break } 103 | 104 | addEditController.setTargetManager(managers[index], title: managers[index].localizedDescription) 105 | 106 | case "introspect-status": 107 | // The user tapped on a configuration while not in editing mode. 108 | guard let statusViewController = segue.destination as? StatusViewController , index < managers.count else { break } 109 | 110 | statusViewController.targetManager = managers[index] 111 | 112 | default: 113 | break 114 | } 115 | } 116 | 117 | /// The current number of configurations. 118 | override var listCount: Int { 119 | return managers.count 120 | } 121 | 122 | /// Returns the localized description of the configuration at the given index. 123 | override func listTextForItemAtIndex(_ index: Int) -> String { 124 | return managers[index].localizedDescription ?? "NoName" 125 | } 126 | 127 | /// Returns the appropriate image for the configuration at the given index. 128 | override func listImageForItemAtIndex(_ index: Int) -> UIImage? { 129 | let manager = managers[index] 130 | 131 | guard manager.isEnabled else { return disabledImage } 132 | 133 | switch manager.connection.status { 134 | case .invalid: fallthrough 135 | case .disconnected: return disconnectedImage 136 | default: return notDisconnectedImage 137 | } 138 | } 139 | 140 | /// Returns UITableViewCellAccessoryType.DisclosureIndicator. 141 | override var listAccessoryType: UITableViewCellAccessoryType { 142 | return .disclosureIndicator 143 | } 144 | 145 | /// Returns UITableViewCellAccessoryType.DetailButton. 146 | override var listEditingAccessoryType: UITableViewCellAccessoryType { 147 | return .detailButton 148 | } 149 | 150 | /// Handle a user tap on the "Delete" button for a configuration. 151 | override func listRemoveItemAtIndex(_ index: Int) { 152 | 153 | // Remove the configuration from the Network Extension preferences. 154 | managers[index].removeFromPreferences { 155 | error in 156 | if let error = error { 157 | simpleTunnelLog("Failed to remove manager: \(error)") 158 | } 159 | } 160 | managers.remove(at: index) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /SimpleTunnel/ConfigurationParametersViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ConfigurationParametersViewController, which is a UITableViewController sub-class that contains a table of configuration parameters. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A table view controller for a view that contains a table of configuration parameter input fields. 12 | class ConfigurationParametersViewController: UITableViewController { 13 | 14 | // MARK: Properties 15 | 16 | /// The cells to display in the table. 17 | var cells = [UITableViewCell]() 18 | 19 | // MARK: UITableViewDataSource 20 | 21 | /// Returns the number of sections in the table (always 1). 22 | override func numberOfSections(in tableView: UITableView) -> Int { 23 | return 1 24 | } 25 | 26 | /// Returns the number of cells currently in the cells list. 27 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 28 | return cells.count 29 | } 30 | 31 | /// Returns the cell at the given index in the cells list. 32 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | return cells[(indexPath as NSIndexPath).row] 34 | } 35 | 36 | // MARK: Interface 37 | 38 | /// Insert or remove cells into the cells list per the current value of a SwitchCell object. 39 | func updateCellsWithDependentsOfCell(_ cell: SwitchCell) { 40 | if let indexPath = getIndexPathOfCell(cell) 41 | , !cell.dependentCells.isEmpty 42 | { 43 | let index = (indexPath as NSIndexPath).row + 1 44 | if cell.isOn { 45 | cells.insert(contentsOf: cell.dependentCells, at: index) 46 | } 47 | else { 48 | let removeRange = index..<(index + cell.dependentCells.count) 49 | cells.removeSubrange(removeRange) 50 | } 51 | } 52 | } 53 | 54 | /// Return the index of a given cell in the cells list. 55 | func getIndexPathOfCell(_ cell: UITableViewCell) -> IndexPath? { 56 | if let row = cells.index(where: { $0 == cell }) { 57 | return IndexPath(row: row, section: 0) 58 | } 59 | return nil 60 | } 61 | 62 | /// Construct a description string for a list of items, given a description of a single item. 63 | func getDescriptionForListValue(_ listValue: [AnyObject]?, itemDescription: String, placeHolder: String = "Optional") -> String { 64 | if let list = listValue , !list.isEmpty { 65 | return "\(list.count) \(itemDescription)" + (list.count > 1 ? "s" : "") 66 | } else { 67 | return placeHolder 68 | } 69 | } 70 | 71 | /// Construct a description string for a list of strings, given a description of a single string. 72 | func getDescriptionForStringList(_ stringList: [String]?, itemDescription: String, placeHolder: String = "Optional") -> String { 73 | if let list = stringList , !list.isEmpty { 74 | 75 | return (list.count <= 3 ? list.joined(separator: ", ") : "\(list.count) \(itemDescription)s") 76 | } else { 77 | return placeHolder 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /SimpleTunnel/ConnectionRuleAddEditController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ConnectionRuleAddEditController class, which controls a view that is used to add or edit a Connect On Demand connection rule. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | // MARK: Extensions 13 | 14 | /// Make NEEvaluateConnectionRuleAction convertible to a string. 15 | extension NEEvaluateConnectionRuleAction: CustomStringConvertible { 16 | public var description: String { 17 | switch self { 18 | case .connectIfNeeded: return "Connect If Needed" 19 | case .neverConnect: return "Never Connect" 20 | } 21 | } 22 | } 23 | 24 | /// A view controller object for a view that contains input fields used to define a Evaluate Connection rule. 25 | class ConnectionRuleAddEditController: ConfigurationParametersViewController { 26 | 27 | // MARK: Properties 28 | 29 | /// A table cell that when tapped allows the user to select the action for the rule. 30 | @IBOutlet weak var actionCell: UITableViewCell! 31 | 32 | /// A table cell that when tapped allows the user to set the DNS Domains match condition for the rule. 33 | @IBOutlet weak var domainsCell: UITableViewCell! 34 | 35 | /// A table cell that when tapped allows the user to set the DNS Servers to be used when the rule matches. 36 | @IBOutlet weak var requiredDNSCell: UITableViewCell! 37 | 38 | /// A table cell that contains a text field where the user inputs the rule's probe URL. 39 | @IBOutlet weak var requiredURLProbeCell: TextFieldCell! 40 | 41 | /// The connection rule being edited or added. 42 | var targetRule = NEEvaluateConnectionRule(matchDomains: [], andAction: .connectIfNeeded) 43 | 44 | /// A block to execute when the user is finished editing the connection rule. 45 | var addRuleHandler: (NEEvaluateConnectionRule) -> Void = { rule in return } 46 | 47 | // MARK: UIViewController 48 | 49 | /// Handle the event when the view is loaded into memory. 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | cells = [ 54 | actionCell, 55 | domainsCell, 56 | requiredDNSCell, 57 | requiredURLProbeCell 58 | ].flatMap { $0 } 59 | 60 | requiredURLProbeCell.valueChanged = { 61 | if let enteredText = self.requiredURLProbeCell.textField.text { 62 | self.targetRule.probeURL = URL(string: enteredText) 63 | } 64 | else { 65 | self.targetRule.probeURL = nil 66 | } 67 | } 68 | } 69 | 70 | /// Handle the event when the view is being displayed. 71 | override func viewWillAppear(_ animated: Bool) { 72 | super.viewWillAppear(animated) 73 | 74 | tableView.reloadData() 75 | 76 | actionCell.detailTextLabel?.text = targetRule.action.description 77 | 78 | domainsCell.detailTextLabel?.text = getDescriptionForStringList(targetRule.matchDomains, itemDescription: "domain", placeHolder: "Required") 79 | 80 | requiredDNSCell.detailTextLabel?.text = getDescriptionForStringList(targetRule.useDNSServers, itemDescription: "server") 81 | 82 | requiredURLProbeCell.textField.text = targetRule.probeURL?.absoluteString ?? nil 83 | } 84 | 85 | /// Set up the destination view controller for a segue away from this view controller. 86 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 87 | guard let identifier = segue.identifier else { return } 88 | 89 | switch identifier { 90 | case "edit-use-dns-servers": 91 | // The user tapped on the Use DNS Servers table cell. 92 | guard let stringListController = segue.destination as? StringListController else { break } 93 | 94 | stringListController.setTargetStrings(targetRule.useDNSServers, title: "Use DNS Servers", addTitle: "Add a server address...") { newAddresses in 95 | self.targetRule.useDNSServers = newAddresses 96 | } 97 | 98 | case "edit-connection-rule-action": 99 | // The user tapped on the Action table cell. 100 | guard let enumController = segue.destination as? EnumPickerController else { break } 101 | 102 | let enumValues: [NEEvaluateConnectionRuleAction] = [ .connectIfNeeded, .neverConnect, ], 103 | stringValues = enumValues.flatMap { $0.description }, 104 | currentSelection = enumValues.index { $0 == targetRule.action } 105 | 106 | enumController.setValues(stringValues, title: "Action", currentSelection: currentSelection) { newRow in 107 | let newAction = enumValues[newRow] 108 | guard self.targetRule.action != newAction else { return } 109 | 110 | let newRule = NEEvaluateConnectionRule(matchDomains: self.targetRule.matchDomains, andAction: newAction) 111 | newRule.useDNSServers = self.targetRule.useDNSServers 112 | newRule.probeURL = self.targetRule.probeURL 113 | 114 | self.targetRule = newRule 115 | } 116 | 117 | case "edit-connection-rule-match-domains": 118 | // The user tapped on the Match Domains table cell. 119 | guard let stringListController = segue.destination as? StringListController else { break } 120 | 121 | stringListController.setTargetStrings(targetRule.matchDomains, title: "Match Domains", addTitle: "Add a domain...") { newStrings in 122 | let newRule = NEEvaluateConnectionRule(matchDomains: newStrings, andAction: self.targetRule.action) 123 | newRule.useDNSServers = self.targetRule.useDNSServers 124 | newRule.probeURL = self.targetRule.probeURL 125 | self.targetRule = newRule 126 | } 127 | 128 | default: 129 | break 130 | } 131 | } 132 | 133 | // MARK: Interface 134 | 135 | /// Set the target connection rule, the title of the view, and the block to execute when the user if finished editing the rule. 136 | func setTargetRule(_ rule: NEEvaluateConnectionRule?, title: String, saveHandler: @escaping (NEEvaluateConnectionRule) -> Void) { 137 | if let newRule = rule { 138 | targetRule = newRule 139 | } else { 140 | targetRule = NEEvaluateConnectionRule(matchDomains: [], andAction: .connectIfNeeded) 141 | } 142 | navigationItem.title = title 143 | addRuleHandler = saveHandler 144 | } 145 | 146 | /// Handle the user tapping on the "Done" button. 147 | @IBAction func saveTargetRule(_ sender: AnyObject) { 148 | addRuleHandler(targetRule) 149 | performSegue(withIdentifier: "save-connection-rule", sender: sender) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /SimpleTunnel/ConnectionRuleListController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ConnectionRuleListController class, which controls a list of Connect On Demand connection rules. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// A view controller object for a view that displays a editable list of Evaluate Connection rules. 13 | class ConnectionRuleListController: ListViewController { 14 | 15 | // MARK: Properties 16 | 17 | /// The Connect On Demand rule containing the connection rules being displayed. 18 | var targetRule = NEOnDemandRuleEvaluateConnection() 19 | 20 | /// The number of connection rules. 21 | override var listCount: Int { 22 | return targetRule.connectionRules?.count ?? 0 23 | } 24 | 25 | /// Returns UITableViewCellAccessoryType.DetailButton. 26 | override var listAccessoryType: UITableViewCellAccessoryType { 27 | return .detailButton 28 | } 29 | 30 | /// Returns UITableViewCellAccessoryType.DetailButton. 31 | override var listEditingAccessoryType: UITableViewCellAccessoryType { 32 | return .detailButton 33 | } 34 | 35 | /// The text to display in the "add a new item" table cell. 36 | override var listAddButtonText: String { 37 | return "Add Connection Rule..." 38 | } 39 | 40 | // MARK: UIViewController 41 | 42 | /// Handle the event where the view is loaded into memory. 43 | override func viewDidLoad() { 44 | isAddEnabled = true 45 | isAlwaysEditing = true 46 | super.viewDidLoad() 47 | } 48 | 49 | // MARK: ListViewController 50 | 51 | /// Set up the destination view controller for a segue triggered by the user tapping on a cell. 52 | override func listSetupSegue(_ segue: UIStoryboardSegue, forItemAtIndex index: Int) { 53 | guard let identifier = segue.identifier, 54 | let ruleAddEditController = segue.destination as? ConnectionRuleAddEditController 55 | else { return } 56 | 57 | switch identifier { 58 | case "edit-connection-rule": 59 | // The user tapped on a rule in the connection rule list. 60 | guard let connectionRule = targetRule.connectionRules?[index] else { break } 61 | ruleAddEditController.setTargetRule(connectionRule, title: "Connection Rule") { newRule in 62 | self.targetRule.connectionRules?[index] = newRule 63 | } 64 | 65 | case "add-connection-rule": 66 | // The user tapped on the "add a new rule" cell. 67 | ruleAddEditController.setTargetRule(nil, title: "Add Connection Rule") { newRule in 68 | self.targetRule.connectionRules?.append(newRule) 69 | } 70 | 71 | default: 72 | break 73 | } 74 | } 75 | 76 | /// Return the description of the rule at the given index in the list. 77 | override func listTextForItemAtIndex(_ index: Int) -> String { 78 | return targetRule.connectionRules?[index].action.description ?? "" 79 | } 80 | 81 | /// Remove the rule at the given index in the list. 82 | override func listRemoveItemAtIndex(_ index: Int) { 83 | targetRule.connectionRules?.remove(at: index) 84 | } 85 | 86 | // MARK: Interface 87 | 88 | /// Handle an unwind segue back to this view controller. 89 | @IBAction func handleUnwind(_ sender: UIStoryboardSegue) { 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /SimpleTunnel/ContentFilterController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ContentFilterController class, which controls a view used to start and stop a content filter, and display current filtering rules. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | import SimpleTunnelServices 12 | 13 | /// A view controller object for a view that displays Content Filter configuration and rules. 14 | class ContentFilterController: UITableViewController { 15 | 16 | // MARK: Properties 17 | 18 | /// A table view cell that contains the status of the filter. 19 | @IBOutlet weak var statusCell: SwitchCell! 20 | 21 | /// A table view cell that contains the 22 | @IBOutlet weak var rulesServerCell: TextFieldCell! 23 | 24 | /// A variable to pass as the context to addObserver() 25 | var rulesContext = 0 26 | 27 | /// The current list of filtering rules 28 | var currentRules = [(String, String)]() 29 | 30 | // MARK: Initializers 31 | 32 | deinit { 33 | var context = self 34 | FilterUtilities.defaults?.removeObserver(self, forKeyPath: "rules", context:&context) 35 | } 36 | 37 | // MARK: UIViewController 38 | 39 | /// Handle the event where the view is loaded into memory. 40 | override func viewDidLoad() { 41 | FilterUtilities.defaults?.addObserver(self, forKeyPath: "rules", options: NSKeyValueObservingOptions.initial, context:&rulesContext) 42 | 43 | statusCell.valueChanged = { 44 | if self.statusCell.isOn && NEFilterManager.shared().providerConfiguration == nil { 45 | let newConfiguration = NEFilterProviderConfiguration() 46 | newConfiguration.username = "TestUser" 47 | newConfiguration.organization = "Acme Inc." 48 | newConfiguration.filterBrowsers = true 49 | newConfiguration.filterSockets = true 50 | newConfiguration.serverAddress = self.rulesServerCell.textField.text ?? "my.great.filter.server" 51 | NEFilterManager.shared().providerConfiguration = newConfiguration 52 | } 53 | NEFilterManager.shared().isEnabled = self.statusCell.isOn 54 | NEFilterManager.shared().saveToPreferences { error in 55 | if let saveError = error { 56 | simpleTunnelLog("Failed to save the filter configuration: \(saveError)") 57 | self.statusCell.isOn = false 58 | return 59 | } 60 | self.rulesServerCell.textField.text = NEFilterManager.shared().providerConfiguration?.serverAddress 61 | FilterUtilities.defaults?.setValue(NEFilterManager.shared().providerConfiguration?.serverAddress, forKey: "serverAddress") 62 | } 63 | } 64 | 65 | rulesServerCell.valueChanged = { 66 | guard let serverIPAddress = self.rulesServerCell.textField.text , !serverIPAddress.isEmpty else { return } 67 | 68 | NEFilterManager.shared().providerConfiguration?.serverAddress = serverIPAddress 69 | NEFilterManager.shared().saveToPreferences { error in 70 | if let saveError = error { 71 | simpleTunnelLog("Failed to save the filter configuration: \(saveError)") 72 | return 73 | } 74 | 75 | FilterUtilities.defaults?.setValue(serverIPAddress, forKey: "serverAddress") 76 | } 77 | } 78 | } 79 | 80 | /// Handle the event where the view is loaded into memory. 81 | override func viewWillAppear(_ animated: Bool) { 82 | super.viewWillAppear(animated) 83 | NEFilterManager.shared().loadFromPreferences { error in 84 | if let loadError = error { 85 | simpleTunnelLog("Failed to load the filter configuration: \(loadError)") 86 | self.statusCell.isOn = false 87 | return 88 | } 89 | 90 | self.statusCell.isOn = NEFilterManager.shared().isEnabled 91 | self.rulesServerCell.textField.text = NEFilterManager.shared().providerConfiguration?.serverAddress 92 | 93 | self.reloadRules() 94 | } 95 | } 96 | 97 | // MARK: NSObject 98 | 99 | /// Handle changes to the rules 100 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 101 | if context == &rulesContext && keyPath == "rules" { 102 | reloadRules() 103 | } else { 104 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 105 | } 106 | } 107 | 108 | // MARK: UITableViewDataSource 109 | 110 | /// Return the number of sections to display. 111 | override func numberOfSections(in tableView: UITableView) -> Int { 112 | return !currentRules.isEmpty ? 2 : 1 113 | } 114 | 115 | /// Return the number of rows in a section. 116 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 117 | switch section { 118 | case 0: return 2 119 | case 1: return currentRules.count 120 | default: return 0 121 | } 122 | } 123 | 124 | /// Return the cell for given index path. 125 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 126 | if (indexPath as NSIndexPath).section == 0 { 127 | return (indexPath as NSIndexPath).row == 0 ? statusCell : rulesServerCell 128 | } 129 | else if let cell = tableView.dequeueReusableCell(withIdentifier: "rule-cell") { 130 | let (hostString, actionString) = currentRules[(indexPath as NSIndexPath).row] 131 | cell.textLabel?.text = hostString 132 | cell.detailTextLabel?.text = actionString 133 | return cell 134 | } 135 | else { 136 | return UITableViewCell() 137 | } 138 | } 139 | 140 | /// Return the title for a section in the table. 141 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 142 | return section == 1 ? "Current Rules" : nil 143 | } 144 | 145 | // MARK: Interface 146 | 147 | /// Re-load the current filerting rules from the defaults. 148 | func reloadRules() { 149 | currentRules = [(String, String)]() 150 | 151 | guard let rules = FilterUtilities.defaults?.object(forKey: "rules") as? [String : [String : AnyObject]] else { return } 152 | 153 | for (hostname, ruleInfo) in rules { 154 | guard let ruleActionNum = ruleInfo["kRule"] as? Int, 155 | let ruleAction = FilterRuleAction(rawValue: ruleActionNum) 156 | else { continue } 157 | 158 | currentRules.append((hostname as String, ruleAction.description)) 159 | } 160 | tableView.reloadData() 161 | } 162 | 163 | /// Download a new set of filtering rules from the server. 164 | @IBAction func fetchRulesButtonTouchUpInside(_ sender: UIButton) { 165 | FilterUtilities.fetchRulesFromServer(NEFilterManager.shared().providerConfiguration?.serverAddress) 166 | reloadRules() 167 | } 168 | 169 | } 170 | -------------------------------------------------------------------------------- /SimpleTunnel/EnumPickerController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the EnumPickerController class, which is used to control a view containing a UIPickerView. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A view controller for a view that contains a picker view for selecting a value in an enum. 12 | class EnumPickerController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate { 13 | 14 | // MARK: Properties 15 | 16 | /// The values that can be selected in the picker view. 17 | var enumValues = [String]() 18 | 19 | /// The index of the currently-selected value. 20 | var currentValue: Int? 21 | 22 | /// The picker view. 23 | @IBOutlet weak var enumPicker: UIPickerView! 24 | 25 | /// The title to display for the view. 26 | var enumTitle: String? 27 | 28 | /// A block to execute when the selected value changes. 29 | var selectionChangeHandler: (Int) -> Void = { newRow in return } 30 | 31 | // MARK: Interface 32 | 33 | /// Set the enum values to display, the title of the view, the index of the currently-selected value, and a block to execute when the selected value changes. 34 | func setValues(_ values: [String], title: String, currentSelection: Int?, selectionChanged: @escaping (Int) -> Void) { 35 | enumValues = values 36 | enumTitle = title 37 | currentValue = currentSelection 38 | selectionChangeHandler = selectionChanged 39 | } 40 | 41 | // MARK: UIViewController 42 | 43 | /// Handle the event when the view is being displayed. 44 | override func viewWillAppear(_ animated: Bool) { 45 | super.viewWillAppear(animated) 46 | navigationItem.title = enumTitle 47 | 48 | enumPicker.reloadAllComponents() 49 | if let row = currentValue { 50 | enumPicker.selectRow(row, inComponent: 0, animated: false) 51 | } 52 | } 53 | 54 | // MARK: UIPickerViewDataSource 55 | 56 | /// Return the number of components in the picker, always returns 1. 57 | func numberOfComponents(in pickerView: UIPickerView) -> Int { 58 | return 1 59 | } 60 | 61 | /// Returns the number of enum values. 62 | func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 63 | return enumValues.count 64 | } 65 | 66 | // MARK: UIPickerViewDelegate 67 | 68 | /// Returns the enum value at the given row. 69 | func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { 70 | return NSAttributedString(string: enumValues[row]) 71 | } 72 | 73 | /// Handle the user selecting a value in the picker. 74 | func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { 75 | selectionChangeHandler(row) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GrayDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "GrayDot@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "GrayDot.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "GrayDot@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@1x.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@3x.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GreenDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "GreenDot@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "GreenDot.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "GreenDot@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@1x.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@3x.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/RedDot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "RedDot@1x.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "RedDot.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "RedDot@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@1x.png -------------------------------------------------------------------------------- /SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/below/SimpleTunnel/4fc75ccc8240e31a704ddad7c034fead15bc912e/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@3x.png -------------------------------------------------------------------------------- /SimpleTunnel/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | UIInterfaceOrientationPortraitUpsideDown 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UIVPNPlugin 48 | 49 | ${PLUGIN_BUNDLE_ID} 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /SimpleTunnel/ListViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ListViewController class, which is responsible for controlling a list items. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A view controller base class for a view that displays a list of items in table form. 12 | class ListViewController: UITableViewController { 13 | 14 | // MARK: Properties 15 | 16 | /// A flag indicating that UI controls for adding an item to the list should be displayed. 17 | var isAddEnabled = false 18 | 19 | /// A flag indicating that UI controls for adding an item to the list should always be displayed. 20 | var isAlwaysEditing = false 21 | 22 | /// A custom cell for adding items to the list. 23 | var addCell: UITableViewCell? 24 | 25 | /// The number of items currently in the list. 26 | var listCount: Int { return 0 } 27 | 28 | /// The type of table view cell accessory image to display for each item in the list. 29 | var listAccessoryType: UITableViewCellAccessoryType { 30 | return .none 31 | } 32 | 33 | /// The type of table view cell accessory image to display for each item in the list while the list is being edited. 34 | var listEditingAccessoryType: UITableViewCellAccessoryType { 35 | return .none 36 | } 37 | 38 | /// The type of selection feedback to display items in the list. 39 | var listCellSelectionStyle: UITableViewCellSelectionStyle { 40 | return .default 41 | } 42 | 43 | /// The text to display in the "add a new item" cell. 44 | var listAddButtonText: String { 45 | return "Add Configuration..." 46 | } 47 | 48 | // MARK: Initializers 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | super.init(coder: aDecoder) 52 | } 53 | 54 | init() { 55 | super.init(style: .grouped) 56 | } 57 | 58 | // MARK: UIViewController 59 | 60 | /// Handle the event of the view being loaded into memory. 61 | override func viewDidLoad() { 62 | super.viewDidLoad() 63 | if isAlwaysEditing { 64 | tableView.isEditing = true 65 | } 66 | else { 67 | navigationItem.rightBarButtonItem = editButtonItem 68 | } 69 | } 70 | 71 | /// Handle the event of the view being displayed. 72 | override func viewWillAppear(_ animated: Bool) { 73 | super.viewWillAppear(animated) 74 | tableView.reloadData() 75 | } 76 | 77 | /// Prepare for a segue away from this view controller. 78 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 79 | guard let cell = sender as? UITableViewCell, 80 | let indexPath = tableView.indexPath(for: cell) 81 | else { return } 82 | 83 | listSetupSegue(segue, forItemAtIndex: (indexPath as NSIndexPath).row) 84 | } 85 | 86 | // MARK: UITableView 87 | 88 | /// Enable editing mode on the UITableView. 89 | override func setEditing(_ editing: Bool, animated: Bool) { 90 | super.setEditing(editing, animated: animated) 91 | 92 | if isAddEnabled { 93 | // Insert or delete the last row in the table (i.e., the "add a new item" row). 94 | let indexPath = IndexPath(item: listCount, section: 0) 95 | if editing { 96 | tableView.insertRows(at: [indexPath], with: .bottom) 97 | } 98 | else { 99 | tableView.deleteRows(at: [indexPath], with: .bottom) 100 | } 101 | } 102 | } 103 | 104 | // MARK: UITableViewDataSource 105 | 106 | /// Always returns 1. 107 | override func numberOfSections(in tableView: UITableView) -> Int { 108 | return 1 109 | } 110 | 111 | /// Returns the number of items in the list, incremented by 1 if the list is in edit mode. 112 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 113 | var count = listCount 114 | if tableView.isEditing { 115 | count += 1 116 | } 117 | return count 118 | } 119 | 120 | /// Returns a cell displaying details about the item at the given index path. 121 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 122 | var cell = UITableViewCell() 123 | if (indexPath as NSIndexPath).item < listCount { 124 | // Return a cell containing details about an item in the list. 125 | if let itemCell = tableView.dequeueReusableCell(withIdentifier: "item-cell") { 126 | cell = itemCell 127 | cell.textLabel?.text = listTextForItemAtIndex((indexPath as NSIndexPath).row) 128 | cell.accessoryType = listAccessoryType 129 | cell.editingAccessoryType = listEditingAccessoryType 130 | cell.imageView?.image = listImageForItemAtIndex((indexPath as NSIndexPath).row) 131 | } 132 | } 133 | else if tableView.isEditing && (indexPath as NSIndexPath).item == listCount { 134 | // The list is in edit mode, return the appropriate "add" cell. 135 | if addCell != nil { 136 | cell = addCell! 137 | } 138 | else if let addButtonCell = tableView.dequeueReusableCell(withIdentifier: "add-button") { 139 | cell = addButtonCell 140 | cell.textLabel?.text = listAddButtonText 141 | cell.editingAccessoryType = .none 142 | } 143 | } 144 | 145 | return cell 146 | } 147 | 148 | /// Always returns true, all cells can be edited in the list 149 | override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 150 | return true 151 | } 152 | 153 | /// Make changes to the list per the given editing style and target row. 154 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 155 | switch editingStyle { 156 | case .delete: 157 | listRemoveItemAtIndex((indexPath as NSIndexPath).row) 158 | tableView.deleteRows(at: [ indexPath ], with: .bottom) 159 | default: 160 | break 161 | } 162 | } 163 | 164 | // MARK: UITableViewDelegate 165 | 166 | /// Return the editing style for a row in the table. Returns "Delete" editing style for all items except for the last item, which uses the "Insert" style. 167 | override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle { 168 | if (indexPath as NSIndexPath).item < listCount { 169 | return .delete 170 | } 171 | else { 172 | return .insert 173 | } 174 | } 175 | 176 | // MARK: Interface 177 | 178 | /// Prepare to segue away from this view controller as the result of the user tapping on an item in the list. 179 | func listSetupSegue(_ segue: UIStoryboardSegue, forItemAtIndex index: Int) { 180 | } 181 | 182 | /// Insert a new item into the list. 183 | func listInsertItemAtIndex(_ index: Int) { 184 | tableView.insertRows(at: [ IndexPath(row: index, section: 0) ], with: .bottom) 185 | } 186 | 187 | /// Get the text to display for an item in the list. 188 | func listTextForItemAtIndex(_ index: Int) -> String { 189 | return "" 190 | } 191 | 192 | /// Get the image to display for an item in the list. 193 | func listImageForItemAtIndex(_ index: Int) -> UIImage? { 194 | return nil 195 | } 196 | 197 | /// Remove an item from the list. 198 | func listRemoveItemAtIndex(_ index: Int) { 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /SimpleTunnel/OnDemandRuleAddEditController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the OnDemandRuleAddEditController class, which controls a view that is used to create or edit a Connect On Demand rule. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | // MARK: Extensions 13 | 14 | /// Make NEOnDemandRuleAction convertible to a string 15 | extension NEOnDemandRuleAction: CustomStringConvertible { 16 | public var description: String { 17 | switch self { 18 | case .connect: return "Connect" 19 | case .disconnect: return "Disconnect" 20 | case .ignore: return "Maintain" 21 | case .evaluateConnection: return "Evaluate Connection" 22 | } 23 | } 24 | } 25 | 26 | /// Make NEOnDemandRuleInterfaceType convertible to a string 27 | extension NEOnDemandRuleInterfaceType: CustomStringConvertible { 28 | public var description: String { 29 | switch self { 30 | case .any: return "Any" 31 | case .wiFi: return "Wi-Fi" 32 | case .cellular: return "Cellular" 33 | default: return "" 34 | } 35 | } 36 | } 37 | 38 | /// A view controller object containing input fields used to create or edit a Connect On Demand rule. 39 | class OnDemandRuleAddEditController: ConfigurationParametersViewController { 40 | 41 | // MARK: Properties 42 | 43 | /// A table view cell that when tapped allows the user to select the rule action. 44 | @IBOutlet weak var actionCell: UITableViewCell! 45 | 46 | /// A table view cell that when tapped allows the user to define the DNS Search Domains match condition. 47 | @IBOutlet weak var DNSSearchDomainsCell: UITableViewCell! 48 | 49 | /// A table view cell that when tapped allows the user to define the DNS Server match condition. 50 | @IBOutlet weak var DNSServersCell: UITableViewCell! 51 | 52 | /// A table view cell that when tapped allows the user to define the network interface type match condition. 53 | @IBOutlet weak var interfaceTypeCell: UITableViewCell! 54 | 55 | /// A table view cell that when tapped allows the user to define the SSID match condition. 56 | @IBOutlet weak var SSIDsCell: UITableViewCell! 57 | 58 | /// A table view cell that when tapped allows the user to define the URL probe match condition. 59 | @IBOutlet weak var URLProbeCell: TextFieldCell! 60 | 61 | /// A table view cell that when tapped allows the user to define the connection match rules. 62 | @IBOutlet weak var connectionRulesCell: UITableViewCell! 63 | 64 | /// The Connect On Demand rule being added or edited. 65 | var targetRule: NEOnDemandRule = NEOnDemandRuleEvaluateConnection() 66 | 67 | /// The block to execute when the user finishes editing the rule. 68 | var addRuleHandler: (NEOnDemandRule) -> Void = { rule in return } 69 | 70 | // MARK: UIViewController 71 | 72 | /// Handle the event when the view is loaded into memory. 73 | override func viewDidLoad() { 74 | super.viewDidLoad() 75 | 76 | // Set up the table cells. 77 | 78 | cells = [ 79 | actionCell, 80 | DNSSearchDomainsCell, 81 | DNSServersCell, 82 | interfaceTypeCell, 83 | SSIDsCell, 84 | URLProbeCell 85 | ].flatMap { $0 } 86 | 87 | URLProbeCell.valueChanged = { 88 | if let enteredText = self.URLProbeCell.textField.text { 89 | self.targetRule.probeURL = URL(string: enteredText) 90 | } 91 | else { 92 | self.targetRule.probeURL = nil 93 | } 94 | } 95 | } 96 | 97 | /// Handle the event when the view is being displayed. 98 | override func viewWillAppear(_ animated: Bool) { 99 | super.viewWillAppear(animated) 100 | 101 | tableView.reloadData() 102 | 103 | // Set the cell contents per the current rule settings. 104 | 105 | updateConnectionRulesCell() 106 | 107 | actionCell.detailTextLabel?.text = targetRule.action.description 108 | 109 | DNSSearchDomainsCell.detailTextLabel?.text = getDescriptionForStringList(targetRule.dnsSearchDomainMatch, itemDescription: "domain") 110 | 111 | DNSServersCell.detailTextLabel?.text = getDescriptionForStringList(targetRule.dnsServerAddressMatch, itemDescription: "server") 112 | 113 | interfaceTypeCell.detailTextLabel?.text = targetRule.interfaceTypeMatch.description 114 | 115 | SSIDsCell.detailTextLabel?.text = getDescriptionForStringList(targetRule.ssidMatch, itemDescription: "SSID") 116 | 117 | if let evaluateRule = targetRule as? NEOnDemandRuleEvaluateConnection { 118 | connectionRulesCell.detailTextLabel?.text = getDescriptionForListValue(evaluateRule.connectionRules, itemDescription: "rule", placeHolder: "Required") 119 | } 120 | 121 | URLProbeCell.textField.text = targetRule.probeURL?.absoluteString ?? nil 122 | } 123 | 124 | /// Set up the destination view controller of a segue away from this view controller. 125 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 126 | guard let identifier = segue.identifier else { return } 127 | 128 | switch identifier { 129 | case "edit-ssids": 130 | // The user tapped on the SSIDs cell. 131 | guard let stringListController = segue.destination as? StringListController else { break } 132 | 133 | stringListController.setTargetStrings(targetRule.ssidMatch, title: "Match SSIDs", addTitle: "Add a SSID...") { newSSIDs in 134 | self.targetRule.ssidMatch = newSSIDs 135 | } 136 | 137 | case "edit-interface-type-match": 138 | // The user tapped on the Interface Type cell. 139 | guard let enumController = segue.destination as? EnumPickerController else { break } 140 | 141 | let enumValues: [NEOnDemandRuleInterfaceType] = [ .any, .wiFi, .cellular ], 142 | stringValues = enumValues.flatMap { $0.description }, 143 | currentSelection = enumValues.index { $0 == targetRule.interfaceTypeMatch } 144 | 145 | enumController.setValues(stringValues, title: "Interface Type", currentSelection: currentSelection) { newRow in 146 | self.targetRule.interfaceTypeMatch = enumValues[newRow] 147 | } 148 | 149 | case "edit-dns-servers": 150 | // The user tapped on the DNS Servers cell. 151 | guard let stringListController = segue.destination as? StringListController else { break } 152 | 153 | stringListController.setTargetStrings(targetRule.dnsServerAddressMatch, title: "Match DNS Servers", addTitle: "Add a server address...") { newAddresses in 154 | self.targetRule.dnsServerAddressMatch = newAddresses 155 | } 156 | 157 | case "edit-dns-search-domains": 158 | // The user tapped on the DNS Search Domains cell. 159 | guard let stringListController = segue.destination as? StringListController else { break } 160 | 161 | stringListController.setTargetStrings(targetRule.dnsSearchDomainMatch, title: "Match DNS Search Domains", addTitle: "Add a search domain...") { newStrings in 162 | self.targetRule.dnsSearchDomainMatch = newStrings 163 | } 164 | 165 | case "edit-on-demand-action": 166 | // The user tapped on the Action cell. 167 | guard let enumController = segue.destination as? EnumPickerController else { break } 168 | 169 | let enumValues: [NEOnDemandRuleAction] = [ .evaluateConnection, .disconnect, .connect, .ignore ], 170 | stringValues = enumValues.flatMap { $0.description }, 171 | currentSelection = enumValues.index { $0 == targetRule.action } 172 | 173 | enumController.setValues(stringValues, title: "Action", currentSelection: currentSelection) { newRow in 174 | self.changeTargetRuleType(enumValues[newRow]) 175 | } 176 | 177 | case "edit-connection-rules": 178 | // The user tapped on the Connection Rules cell. 179 | guard let connRuleListController = segue.destination as? ConnectionRuleListController, 180 | let rule = targetRule as? NEOnDemandRuleEvaluateConnection 181 | else { break } 182 | 183 | if rule.connectionRules == nil { 184 | rule.connectionRules = [] 185 | } 186 | 187 | connRuleListController.targetRule = rule 188 | 189 | default: 190 | break 191 | } 192 | } 193 | 194 | /// Set the target rule to add or edit, the title of the view, and the block to execute when the user is finished editing the rule. 195 | func setTargetRule(_ rule: NEOnDemandRule?, title: String, saveRuleHandler: @escaping (NEOnDemandRule) -> Void) { 196 | if let newRule = rule { 197 | // Edit a copy of the given rule. 198 | targetRule = newRule.copy() as! NEOnDemandRule 199 | } else { 200 | targetRule = NEOnDemandRuleEvaluateConnection() 201 | } 202 | navigationItem.title = title 203 | addRuleHandler = saveRuleHandler 204 | } 205 | 206 | /// Set the target rule to a new rule with all the same match conditions as the current target rule, but with a different action. 207 | func changeTargetRuleType(_ newAction: NEOnDemandRuleAction) { 208 | guard targetRule.action != newAction else { return } 209 | let newRule: NEOnDemandRule 210 | 211 | switch newAction { 212 | case .evaluateConnection: 213 | newRule = NEOnDemandRuleEvaluateConnection() 214 | 215 | case .connect: 216 | newRule = NEOnDemandRuleConnect() 217 | 218 | case .disconnect: 219 | newRule = NEOnDemandRuleDisconnect() 220 | 221 | case .ignore: 222 | newRule = NEOnDemandRuleIgnore() 223 | } 224 | 225 | newRule.dnsSearchDomainMatch = targetRule.dnsSearchDomainMatch 226 | newRule.dnsServerAddressMatch = targetRule.dnsServerAddressMatch 227 | newRule.interfaceTypeMatch = targetRule.interfaceTypeMatch 228 | newRule.ssidMatch = targetRule.ssidMatch 229 | newRule.probeURL = targetRule.probeURL 230 | 231 | targetRule = newRule 232 | 233 | updateConnectionRulesCell() 234 | } 235 | 236 | /// Show or hide the connection rules cell based on the action of the target rule. 237 | func updateConnectionRulesCell() { 238 | guard let actionIndexPath = self.getIndexPathOfCell(actionCell) else { return } 239 | 240 | if let rulesIndexPath = self.getIndexPathOfCell(connectionRulesCell) { 241 | // The connection rules cell is being displayed. If the action is not "Evaluate Connection", then remove the connection rules cell. 242 | if targetRule.action != .evaluateConnection { 243 | cells.remove(at: (rulesIndexPath as NSIndexPath).row) 244 | self.tableView.deleteRows(at: [ rulesIndexPath ], with: .bottom) 245 | } 246 | } else { 247 | // The connection rules cell is not being displayed. If the action is "Evaluate Connection", then insert the connection rules cell. 248 | if targetRule.action == .evaluateConnection { 249 | cells.insert(connectionRulesCell, at: (actionIndexPath as NSIndexPath).row + 1) 250 | let indexPaths = [ IndexPath(row: (actionIndexPath as NSIndexPath).row + 1, section: (actionIndexPath as NSIndexPath).section) ] 251 | self.tableView.insertRows(at: indexPaths, with: .bottom) 252 | } 253 | } 254 | } 255 | 256 | /// Handle the user tapping on the "Done" button. 257 | @IBAction func saveTargetRule(_ sender: AnyObject) { 258 | addRuleHandler(targetRule) 259 | 260 | // Transition back to the Connect On Demand rule list view. 261 | self.performSegue(withIdentifier: "save-on-demand-rule", sender: sender) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /SimpleTunnel/OnDemandRuleListController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the OnDemandRuleListController class, which is responsible for controlling a list of Connect On Demand rules. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// A view controller for a view displaying a list of Connect On Demand rules. 13 | class OnDemandRuleListController: ListViewController { 14 | 15 | // MARK: Properties 16 | 17 | /// The VPN configuration containing the Connect On Demand rules. 18 | var targetManager: NEVPNManager = NEVPNManager.shared() 19 | 20 | /// The text to display in the list's "add new item" row. 21 | override var listAddButtonText: String { 22 | return "Add On Demand Rule..." 23 | } 24 | 25 | /// The number of Connect On Demand rules. 26 | override var listCount: Int { 27 | return targetManager.onDemandRules?.count ?? 0 28 | } 29 | 30 | /// Returns UITableViewCellAccessoryType.DetailButton 31 | override var listAccessoryType: UITableViewCellAccessoryType { 32 | return .detailButton 33 | } 34 | 35 | /// Returns UITableViewCellAccessoryType.DetailButton 36 | override var listEditingAccessoryType: UITableViewCellAccessoryType { 37 | return .detailButton 38 | } 39 | 40 | // MARK: UIViewController 41 | 42 | /// Handle the event when the view is loaded into memory. 43 | override func viewDidLoad() { 44 | isAddEnabled = true 45 | isAlwaysEditing = true 46 | super.viewDidLoad() 47 | } 48 | 49 | // MARK: Interface 50 | 51 | /// Handle unwind segues to this view controller. 52 | @IBAction func handleUnwind(_ sender: UIStoryboardSegue) { 53 | } 54 | 55 | // MARK: ListViewController 56 | 57 | /// Set up the destination view controller of a segue away from this view controller. 58 | override func listSetupSegue(_ segue: UIStoryboardSegue, forItemAtIndex index: Int) { 59 | guard let identifier = segue.identifier, 60 | let ruleAddEditController = segue.destination as? OnDemandRuleAddEditController 61 | else { return } 62 | 63 | switch identifier { 64 | case "edit-on-demand-rule": 65 | // The user tapped on the editing accessory of a rule in the list. 66 | guard let rule = targetManager.onDemandRules?[index] else { break } 67 | ruleAddEditController.setTargetRule(rule, title: "On Demand Rule") { newRule in 68 | self.targetManager.onDemandRules?[index] = newRule 69 | } 70 | 71 | case "add-on-demand-rule": 72 | // The user tapped on the "add a new rule" row. 73 | ruleAddEditController.setTargetRule(nil, title: "Add On Demand Rule") { newRule in 74 | if self.targetManager.onDemandRules == nil { 75 | self.targetManager.onDemandRules = [NEOnDemandRule]() 76 | } 77 | self.targetManager.onDemandRules?.append(newRule) 78 | } 79 | 80 | default: 81 | break 82 | } 83 | } 84 | 85 | /// Return a description of the rule at the given index in the list. 86 | override func listTextForItemAtIndex(_ index: Int) -> String { 87 | return targetManager.onDemandRules?[index].action.description ?? "" 88 | } 89 | 90 | /// Remove a rule from the list. 91 | override func listRemoveItemAtIndex(_ index: Int) { 92 | targetManager.onDemandRules?.remove(at: index) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SimpleTunnel/ProxyAutoConfigScriptController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ProxyAutoConfigScriptController class, which controls a view used to input the text of a Proxy Auto-Configuration Script. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// A view controller object for a view that contains a text box where the user can enter a Proxy Auto-Configuration (PAC) script. 13 | class ProxyAutoConfigScriptController: UIViewController { 14 | 15 | // MARK: Properties 16 | 17 | /// The text view containing the script. 18 | @IBOutlet weak var scriptText: UITextView! 19 | 20 | /// The block to call when the user taps on the "Done" button. 21 | var saveScriptCallback: (String?) -> Void = { script in return } 22 | 23 | // MARK: Interface 24 | 25 | /// Call the saveScriptCallback and transition back to the proxy settings view. 26 | @IBAction func saveScript(_ sender: AnyObject) { 27 | saveScriptCallback(scriptText.text) 28 | performSegue(withIdentifier: "save-proxy-script", sender: sender) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SimpleTunnel/ProxyServerAddEditController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ProxyServerAddEditController class, which controls a view used to create or edit a proxy server configuration. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// A view controller object for a view that contains input fields used to define HTTP proxy server settings. 13 | class ProxyServerAddEditController: ConfigurationParametersViewController { 14 | 15 | // MARK: Properties 16 | 17 | /// A table view cell containing a text input field where the user enters the proxy server address. 18 | @IBOutlet weak var addressCell: TextFieldCell! 19 | 20 | /// A table view cell containing a text input field where the user enters the proxy server port number. 21 | @IBOutlet weak var portCell: TextFieldCell! 22 | 23 | /// A table view cell containing a text input field where the user enters the username portion of the proxy credential. 24 | @IBOutlet weak var usernameCell: TextFieldCell! 25 | 26 | /// A table view cell containing a text input field where the user enters the password portion of the proxy credential. 27 | @IBOutlet weak var passwordCell: TextFieldCell! 28 | 29 | /// A table view cell containing a switch that toggles authentication for the proxy server. 30 | @IBOutlet weak var authenticationSwitchCell: SwitchCell! 31 | 32 | /// The NEProxyServer object containing the proxy server settings. 33 | var targetServer = NEProxyServer(address: "", port: 0) 34 | 35 | /// The block to call when the user taps on the "Done" button. 36 | var saveChangesCallback: (NEProxyServer) -> Void = { server in return } 37 | 38 | // MARK: UIViewController 39 | 40 | /// Handle the event where the view is loaded into memory. 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | 44 | cells = [ 45 | addressCell, 46 | portCell, 47 | authenticationSwitchCell 48 | ].flatMap { $0 } 49 | 50 | authenticationSwitchCell.dependentCells = [ usernameCell, passwordCell ] 51 | authenticationSwitchCell.getIndexPath = { 52 | return self.getIndexPathOfCell(self.authenticationSwitchCell) 53 | } 54 | authenticationSwitchCell.valueChanged = { 55 | self.updateCellsWithDependentsOfCell(self.authenticationSwitchCell) 56 | self.targetServer.authenticationRequired = self.authenticationSwitchCell.isOn 57 | } 58 | } 59 | 60 | /// Handle the event when the view is being displayed. 61 | override func viewWillAppear(_ animated: Bool) { 62 | super.viewWillAppear(animated) 63 | 64 | tableView.reloadData() 65 | 66 | addressCell.textField.text = !targetServer.address.isEmpty ? targetServer.address : nil 67 | portCell.textField.text = targetServer.port > 0 ? String(targetServer.port) : nil 68 | passwordCell.textField.text = targetServer.password 69 | usernameCell.textField.text = targetServer.username 70 | authenticationSwitchCell.isOn = targetServer.authenticationRequired 71 | } 72 | 73 | // MARK: Interface 74 | 75 | /// Set the NEProxyServer object to modify, the title of the view, and a block to call when the user is done modify the proxy server settings. 76 | func setTargetServer(_ server: NEProxyServer?, title: String, saveHandler: @escaping (NEProxyServer) -> Void) { 77 | targetServer = server ?? NEProxyServer(address: "", port: 0) 78 | navigationItem.title = title 79 | saveChangesCallback = saveHandler 80 | } 81 | 82 | /// Gather all of the inputs from the user and call saveChangesCallback. This function is called when the user taps on the "Done" button. 83 | @IBAction func saveProxyServer(_ sender: AnyObject) { 84 | guard let address = addressCell.textField.text, 85 | let portString = portCell.textField.text, 86 | let port = Int(portString) 87 | , !address.isEmpty && !portString.isEmpty 88 | else { return } 89 | 90 | let result = NEProxyServer(address: address, port: port) 91 | result.username = usernameCell.textField.text 92 | result.password = passwordCell.textField.text 93 | result.authenticationRequired = authenticationSwitchCell.isOn 94 | saveChangesCallback(result) 95 | // Go back to the main proxy settings view. 96 | performSegue(withIdentifier: "save-proxy-server-settings", sender: sender) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /SimpleTunnel/ProxySettingsController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ProxySettingsController class, which controls a view used to edit proxy settings. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | 12 | /// A view controller object for a view that contains input fields used to define a HTTP proxy settings. 13 | class ProxySettingsController: ConfigurationParametersViewController { 14 | 15 | // MARK: Properties 16 | 17 | /// A table view cell containing a switch that toggles Proxy Auto Configuration 18 | @IBOutlet weak var pacSwitchCell: SwitchCell! 19 | 20 | /// A table view cell containing a text input field where the user can enter the URL for a Proxy Auto Configuration script. 21 | @IBOutlet weak var pacURLCell: TextFieldCell! 22 | 23 | /// A table view cell that when tapped on allows the user to enter a Proxy Auto Configuration script. 24 | @IBOutlet weak var pacScriptCell: UITableViewCell! 25 | 26 | /// A table view cell that when tapped on allows the user to modify the static HTTP proxy settings. 27 | @IBOutlet weak var HTTPCell: UITableViewCell! 28 | 29 | /// A table view cell containing a switch that toggles the use of a static HTTP proxy. 30 | @IBOutlet weak var HTTPSwitchCell: SwitchCell! 31 | 32 | /// A table view cell that when tapped on allows the user to modify the static HTTPS proxy settings. 33 | @IBOutlet weak var HTTPSCell: UITableViewCell! 34 | 35 | /// A table view cell containing a switch that toggles the use of a static HTTPS proxy. 36 | @IBOutlet weak var HTTPSSwitchCell: SwitchCell! 37 | 38 | /// A table view cell containing a switch that toggles the exclusion of HTTP requests for "simple" (single-label) hosts from using the HTTP proxy settings. 39 | @IBOutlet weak var excludeSimpleCell: SwitchCell! 40 | 41 | /// A table view cell that when tapped on allows the user to define patterns for host names that will not use the HTTP proxy settings. 42 | @IBOutlet weak var exceptionsCell: UITableViewCell! 43 | 44 | /// A table view cell that when tapped on allows the user to define the domains of hosts that will use the HTTP proxy settings. 45 | @IBOutlet weak var matchDomainsCell: UITableViewCell! 46 | 47 | /// The VPN configuration containing the proxy settings. 48 | var targetConfiguration = NEVPNProtocol() 49 | 50 | // MARK: UIViewController 51 | 52 | /// Handle the event where the view is loaded into memory. 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | cells = [ 57 | pacSwitchCell, 58 | HTTPSwitchCell, 59 | HTTPSSwitchCell, 60 | excludeSimpleCell, 61 | exceptionsCell, 62 | matchDomainsCell 63 | ].flatMap { $0 } 64 | 65 | pacSwitchCell.dependentCells = [ pacURLCell, pacScriptCell ] 66 | pacSwitchCell.getIndexPath = { 67 | return self.getIndexPathOfCell(self.pacSwitchCell) 68 | } 69 | pacSwitchCell.valueChanged = { 70 | self.updateCellsWithDependentsOfCell(self.pacSwitchCell) 71 | self.targetConfiguration.proxySettings?.autoProxyConfigurationEnabled = self.pacSwitchCell.isOn 72 | } 73 | 74 | pacURLCell.valueChanged = { 75 | if let enteredText = self.pacURLCell.textField.text { 76 | self.targetConfiguration.proxySettings?.proxyAutoConfigurationURL = URL(string: enteredText) 77 | } 78 | else { 79 | self.targetConfiguration.proxySettings?.proxyAutoConfigurationURL = nil 80 | } 81 | } 82 | 83 | HTTPSwitchCell.dependentCells = [ HTTPCell ] 84 | HTTPSwitchCell.getIndexPath = { 85 | return self.getIndexPathOfCell(self.HTTPSwitchCell) 86 | } 87 | HTTPSwitchCell.valueChanged = { 88 | self.updateCellsWithDependentsOfCell(self.HTTPSwitchCell) 89 | self.targetConfiguration.proxySettings?.httpEnabled = self.HTTPSwitchCell.isOn 90 | } 91 | 92 | HTTPSSwitchCell.dependentCells = [ HTTPSCell ] 93 | HTTPSSwitchCell.getIndexPath = { 94 | return self.getIndexPathOfCell(self.HTTPSSwitchCell) 95 | } 96 | HTTPSSwitchCell.valueChanged = { 97 | self.updateCellsWithDependentsOfCell(self.HTTPSSwitchCell) 98 | self.targetConfiguration.proxySettings?.httpsEnabled = self.HTTPSSwitchCell.isOn 99 | } 100 | 101 | excludeSimpleCell.valueChanged = { 102 | self.targetConfiguration.proxySettings?.excludeSimpleHostnames = self.excludeSimpleCell.isOn 103 | } 104 | } 105 | 106 | /// Handle the event when the view is being displayed. 107 | override func viewWillAppear(_ animated: Bool) { 108 | super.viewWillAppear(animated) 109 | 110 | tableView.reloadData() 111 | 112 | pacSwitchCell.isOn = targetConfiguration.proxySettings?.autoProxyConfigurationEnabled ?? false 113 | pacURLCell.textField.text = targetConfiguration.proxySettings?.proxyAutoConfigurationURL?.absoluteString 114 | 115 | if let script = targetConfiguration.proxySettings?.proxyAutoConfigurationJavaScript { 116 | pacScriptCell.detailTextLabel?.text = script.isEmpty ? "Optional" : "..." 117 | } 118 | else { 119 | pacScriptCell.detailTextLabel?.text = "Optional" 120 | } 121 | 122 | HTTPSwitchCell.isOn = targetConfiguration.proxySettings?.httpEnabled ?? false 123 | if let server = targetConfiguration.proxySettings?.httpServer { 124 | HTTPCell.detailTextLabel?.text = "\(server.address):\(server.port)" 125 | } 126 | else { 127 | HTTPCell.detailTextLabel?.text = nil 128 | } 129 | 130 | HTTPSSwitchCell.isOn = targetConfiguration.proxySettings?.httpsEnabled ?? false 131 | if let server = targetConfiguration.proxySettings?.httpsServer { 132 | HTTPSCell.detailTextLabel?.text = "\(server.address):\(server.port)" 133 | } 134 | else { 135 | HTTPSCell.detailTextLabel?.text = nil 136 | } 137 | 138 | excludeSimpleCell.isOn = targetConfiguration.proxySettings?.excludeSimpleHostnames ?? false 139 | 140 | exceptionsCell.detailTextLabel?.text = self.getDescriptionForStringList(targetConfiguration.proxySettings?.exceptionList, itemDescription: "exception") 141 | 142 | matchDomainsCell.detailTextLabel?.text = self.getDescriptionForStringList(targetConfiguration.proxySettings?.matchDomains, itemDescription: "domain") 143 | } 144 | 145 | /// Set up the destination view controller for a segue away from this view controller. 146 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 147 | guard let identifier = segue.identifier else { return } 148 | 149 | switch identifier { 150 | case "edit-match-domains": 151 | // The user tapped on the "match domains" cell. 152 | guard let stringListController = segue.destination as? StringListController else { break } 153 | stringListController.setTargetStrings(targetConfiguration.proxySettings?.matchDomains, title: "Proxy Match Domains", addTitle: "Add a match domain...") { newStrings in 154 | self.targetConfiguration.proxySettings?.matchDomains = newStrings 155 | } 156 | 157 | case "edit-exceptions": 158 | // The user tapped on the "exceptions" cell. 159 | guard let stringListController = segue.destination as? StringListController else { break } 160 | stringListController.setTargetStrings(targetConfiguration.proxySettings?.exceptionList, title: "Proxy Exception Patterns", addTitle: "Add an exception pattern...") { newStrings in 161 | self.targetConfiguration.proxySettings?.exceptionList = newStrings 162 | } 163 | 164 | case "edit-https-proxy-server": 165 | // The user tapped on the "HTTPS server" cell. 166 | guard let proxyServerController = segue.destination as? ProxyServerAddEditController else { break } 167 | 168 | proxyServerController.setTargetServer(targetConfiguration.proxySettings?.httpsServer, title: "HTTPS Proxy Server") { newServer in 169 | self.targetConfiguration.proxySettings?.httpsServer = newServer 170 | } 171 | 172 | case "edit-http-proxy-server": 173 | // The user tapped on the "HTTP server" cell. 174 | guard let proxyServerController = segue.destination as? ProxyServerAddEditController else { break } 175 | 176 | proxyServerController.setTargetServer(targetConfiguration.proxySettings?.httpServer, title: "HTTP Proxy Server") { newServer in 177 | self.targetConfiguration.proxySettings?.httpServer = newServer 178 | } 179 | 180 | case "edit-pac-script": 181 | // The user tapped on the "proxy auto-configuration script" cell. 182 | guard let pacScriptController = segue.destination as? ProxyAutoConfigScriptController else { break } 183 | 184 | pacScriptController.scriptText?.text = targetConfiguration.proxySettings?.proxyAutoConfigurationJavaScript 185 | pacScriptController.saveScriptCallback = { newScript in 186 | if let script = newScript , !script.isEmpty { 187 | self.targetConfiguration.proxySettings?.proxyAutoConfigurationJavaScript = script 188 | } 189 | else { 190 | self.targetConfiguration.proxySettings?.proxyAutoConfigurationJavaScript = nil 191 | } 192 | } 193 | 194 | default: 195 | break 196 | } 197 | } 198 | 199 | // MARK: Interface 200 | 201 | /// Handle an unwind segue back to this view controller. 202 | @IBAction func handleUnwind(_ sender: UIStoryboardSegue) { 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /SimpleTunnel/SimpleTunnel.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | packet-tunnel-provider 8 | app-proxy-provider 9 | content-filter-provider 10 | 11 | com.apple.developer.networking.vpn.api 12 | 13 | allow-vpn 14 | 15 | keychain-access-groups 16 | 17 | $(AppIdentifierPrefix)com.telekom.SimpleTunnel 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SimpleTunnel/StatusViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the StatusViewController class, which controls a view used to start and stop a VPN connection, and display the status of the VPN connection. 7 | */ 8 | 9 | import UIKit 10 | import NetworkExtension 11 | import SimpleTunnelServices 12 | 13 | // MARK: Extensions 14 | 15 | /// Make NEVPNStatus convertible to a string 16 | extension NEVPNStatus: CustomStringConvertible { 17 | public var description: String { 18 | switch self { 19 | case .disconnected: return "Disconnected" 20 | case .invalid: return "Invalid" 21 | case .connected: return "Connected" 22 | case .connecting: return "Connecting" 23 | case .disconnecting: return "Disconnecting" 24 | case .reasserting: return "Reconnecting" 25 | } 26 | } 27 | } 28 | 29 | /// A view controller object for a view that displays VPN status information and allows the user to start and stop the VPN. 30 | class StatusViewController: UITableViewController { 31 | 32 | // MARK: Properties 33 | 34 | /// A switch that toggles the enabled state of the VPN configuration. 35 | @IBOutlet weak var enabledSwitch: UISwitch! 36 | 37 | /// A switch that starts and stops the VPN. 38 | @IBOutlet weak var startStopToggle: UISwitch! 39 | 40 | /// A label that contains the current status of the VPN. 41 | @IBOutlet weak var statusLabel: UILabel! 42 | 43 | /// The target VPN configuration. 44 | var targetManager = NEVPNManager.shared() 45 | 46 | // MARK: UIViewController 47 | 48 | /// Handle the event where the view is being displayed. 49 | override func viewWillAppear(_ animated: Bool) { 50 | super.viewWillAppear(animated) 51 | 52 | // Initialize the UI 53 | enabledSwitch.isOn = targetManager.isEnabled 54 | startStopToggle.isOn = (targetManager.connection.status != .disconnected && targetManager.connection.status != .invalid) 55 | statusLabel.text = targetManager.connection.status.description 56 | navigationItem.title = targetManager.localizedDescription 57 | 58 | // Register to be notified of changes in the status. 59 | NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: targetManager.connection, queue: OperationQueue.main, using: { notification in 60 | self.statusLabel.text = self.targetManager.connection.status.description 61 | self.startStopToggle.isOn = (self.targetManager.connection.status != .disconnected && self.targetManager.connection.status != .disconnecting && self.targetManager.connection.status != .invalid) 62 | }) 63 | 64 | // Disable the start/stop toggle if the configuration is not enabled. 65 | startStopToggle.isEnabled = enabledSwitch.isOn 66 | 67 | // Send a simple IPC message to the provider, handle the response. 68 | if let session = targetManager.connection as? NETunnelProviderSession, 69 | let message = "Hello Provider".data(using: String.Encoding.utf8) 70 | , targetManager.connection.status != .invalid 71 | { 72 | do { 73 | try session.sendProviderMessage(message) { response in 74 | if response != nil { 75 | let responseString = NSString(data: response!, encoding: String.Encoding.utf8.rawValue) 76 | simpleTunnelLog("Received response from the provider: \(String(describing: responseString))") 77 | } else { 78 | simpleTunnelLog("Got a nil response from the provider") 79 | } 80 | } 81 | } catch { 82 | simpleTunnelLog("Failed to send a message to the provider") 83 | } 84 | } 85 | } 86 | 87 | /// Handle the event where the view is being hidden. 88 | override func viewWillDisappear(_ animated: Bool) { 89 | super.viewWillDisappear(animated) 90 | 91 | // Stop watching for status change notifications. 92 | NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NEVPNStatusDidChange, object: targetManager.connection) 93 | } 94 | 95 | /// Handle the user toggling the "enabled" switch. 96 | @IBAction func enabledToggled(_ sender: AnyObject) { 97 | targetManager.isEnabled = enabledSwitch.isOn 98 | targetManager.saveToPreferences { error in 99 | guard error == nil else { 100 | self.enabledSwitch.isOn = self.targetManager.isEnabled 101 | self.startStopToggle.isEnabled = self.enabledSwitch.isOn 102 | return 103 | } 104 | 105 | self.targetManager.loadFromPreferences { error in 106 | self.enabledSwitch.isOn = self.targetManager.isEnabled 107 | self.startStopToggle.isEnabled = self.enabledSwitch.isOn 108 | } 109 | } 110 | } 111 | 112 | /// Handle the user toggling the "VPN" switch. 113 | @IBAction func startStopToggled(_ sender: AnyObject) { 114 | if targetManager.connection.status == .disconnected || targetManager.connection.status == .invalid { 115 | do { 116 | try targetManager.connection.startVPNTunnel() 117 | } 118 | catch { 119 | simpleTunnelLog("Failed to start the VPN: \(error)") 120 | } 121 | } 122 | else { 123 | targetManager.connection.stopVPNTunnel() 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SimpleTunnel/StringListController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the StringListController class, which controls a list of strings. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A view controller of a view that displays an editable list of strings. 12 | class StringListController: ListViewController { 13 | 14 | // MARK: Properties 15 | 16 | /// The current list of strings. 17 | var targetStrings = [String]() 18 | 19 | /// The text to display in the "add a string" text field. 20 | var addText: String? 21 | 22 | /// The title to display for the list. 23 | var listTitle: String? 24 | 25 | /// The block to execute when the list of strings changes. 26 | var stringsChangedHandler: ([String]) -> Void = { strings in return } 27 | 28 | /// A table view cell containing a text field used to enter new strings to be added to the list. 29 | @IBOutlet weak var addStringCell: TextFieldCell! 30 | 31 | /// The number of strings in the list. 32 | override var listCount: Int { 33 | return targetStrings.count 34 | } 35 | 36 | /// Returns UITableViewCellSelectionStyle.None 37 | override var listCellSelectionStyle: UITableViewCellSelectionStyle { 38 | return .none 39 | } 40 | 41 | // MARK: UIViewController 42 | 43 | /// Handle the event when the view is loaded into memory. 44 | override func viewDidLoad() { 45 | isAddEnabled = true 46 | isAlwaysEditing = true 47 | 48 | addStringCell.valueChanged = { 49 | guard let enteredText = self.addStringCell.textField.text else { return } 50 | 51 | self.targetStrings.append(enteredText) 52 | self.listInsertItemAtIndex(self.targetStrings.count - 1) 53 | self.addStringCell.textField.text = "" 54 | self.stringsChangedHandler(self.targetStrings) 55 | } 56 | 57 | // Set addStringCell as a custom "add a new item" cell. 58 | addCell = addStringCell 59 | 60 | super.viewDidLoad() 61 | } 62 | 63 | /// Handle the event when the view is being displayed. 64 | override func viewWillAppear(_ animated: Bool) { 65 | super.viewWillAppear(animated) 66 | addStringCell.textField.placeholder = addText 67 | navigationItem.title = listTitle 68 | } 69 | 70 | // MARK: ListViewController 71 | 72 | /// Return the string at the given index. 73 | override func listTextForItemAtIndex(_ index: Int) -> String { 74 | return targetStrings[index] 75 | } 76 | 77 | /// Remove the string at the given index. 78 | override func listRemoveItemAtIndex(_ index: Int) { 79 | targetStrings.remove(at: index) 80 | stringsChangedHandler(targetStrings) 81 | } 82 | 83 | // MARK: Interface 84 | 85 | /// Set the list of strings, the title to display for the list, the text used to prompt the user for a new string, and a block to execute when the list of strings changes. 86 | func setTargetStrings(_ strings: [String]?, title: String, addTitle: String, saveHandler: @escaping ([String]) -> Void) { 87 | targetStrings = strings ?? [String]() 88 | listTitle = title 89 | addText = addTitle 90 | stringsChangedHandler = saveHandler 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SimpleTunnel/SwitchCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the SwitchCell class, which is a UITableViewCell sub-class for a cell that contains a UISwitch. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A custom table cell object that contains a switch 12 | class SwitchCell: UITableViewCell { 13 | 14 | // MARK: Properties 15 | 16 | /// The switch contained in the cell. 17 | @IBOutlet weak var toggle: UISwitch! 18 | 19 | /// The table view controller of the table view that contains this cell. Used when inserting and removing cells that are conditionally displayed by the switch. 20 | @IBOutlet weak var tableViewController: UITableViewController! 21 | 22 | /// A list of cells that are inserted/removed from the table based on the value of the switch. 23 | var dependentCells = [UITableViewCell]() 24 | 25 | /// A block to call to get the index path of this cell int its containing table. 26 | var getIndexPath: ((Void) -> IndexPath?)? 27 | 28 | /// A block to call when the value of the switch changes. 29 | var valueChanged: ((Void) -> Void)? 30 | 31 | /// A boolean that toggles the switch. 32 | var isOn: Bool { 33 | get { 34 | return toggle.isOn 35 | } 36 | set (newValue) { 37 | let changed = toggle.isOn != newValue 38 | toggle.isOn = newValue 39 | if changed { 40 | handleValueChanged() 41 | } 42 | } 43 | } 44 | 45 | // MARK: Interface 46 | 47 | /// Handle the user toggling the switch. 48 | @IBAction func toggled(_ sender: AnyObject) { 49 | handleValueChanged() 50 | } 51 | 52 | /// Handle changes in the switch's value. 53 | func handleValueChanged() { 54 | valueChanged?() 55 | 56 | guard let switchIndexPath = getIndexPath?() else { return } 57 | 58 | guard dependentCells.count > 0 else { return } 59 | var indexPaths = [IndexPath]() 60 | 61 | // Create index paths for the dependent cells based on the index path of this cell. 62 | for row in 1...dependentCells.count { 63 | indexPaths.append(IndexPath(row: (switchIndexPath as NSIndexPath).row + row, section: (switchIndexPath as NSIndexPath).section)) 64 | } 65 | 66 | guard !indexPaths.isEmpty else { return } 67 | 68 | if toggle.isOn { 69 | tableViewController.tableView.insertRows(at: indexPaths, with: .bottom) 70 | } else { 71 | tableViewController.tableView.deleteRows(at: indexPaths, with: .bottom) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SimpleTunnel/TextFieldCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the TextFieldCell class, which is a UITableViewCell sub-class for a cell that contains a UITextField. 7 | */ 8 | 9 | import UIKit 10 | 11 | /// A custom table view object that contains a text field. 12 | class TextFieldCell : UITableViewCell, UITextFieldDelegate { 13 | 14 | // MARK: Properties 15 | 16 | /// The text input field. 17 | @IBOutlet weak var textField: UITextField! 18 | 19 | /// The block to call when the value of the text field changes. 20 | var valueChanged: ((Void) -> Void)? 21 | 22 | // MARK: UITextFieldDelegate 23 | 24 | /// Handle the event of the user finishing changing the value of the text field. 25 | func textFieldDidEndEditing(_ textField: UITextField) { 26 | textField.resignFirstResponder() 27 | 28 | valueChanged?() 29 | } 30 | 31 | /// Dismiss the keyboard 32 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 33 | textField.resignFirstResponder() 34 | return true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SimpleTunnelServices/ClientTunnel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ClientTunnel class. The ClientTunnel class implements the client side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | import NetworkExtension 11 | 12 | /// Make NEVPNStatus convertible to a string 13 | extension NWTCPConnectionState: CustomStringConvertible { 14 | public var description: String { 15 | switch self { 16 | case .cancelled: return "Cancelled" 17 | case .connected: return "Connected" 18 | case .connecting: return "Connecting" 19 | case .disconnected: return "Disconnected" 20 | case .invalid: return "Invalid" 21 | case .waiting: return "Waiting" 22 | } 23 | } 24 | } 25 | 26 | /// The client-side implementation of the SimpleTunnel protocol. 27 | open class ClientTunnel: Tunnel { 28 | 29 | // MARK: Properties 30 | 31 | /// The tunnel connection. 32 | open var connection: NWTCPConnection? 33 | 34 | /// The last error that occurred on the tunnel. 35 | open var lastError: NSError? 36 | 37 | /// The previously-received incomplete message data. 38 | var previousData: NSMutableData? 39 | 40 | /// The address of the tunnel server. 41 | open var remoteHost: String? 42 | 43 | // MARK: Interface 44 | 45 | /// Start the TCP connection to the tunnel server. 46 | open func startTunnel(_ provider: NETunnelProvider) -> SimpleTunnelError? { 47 | 48 | guard let serverAddress = provider.protocolConfiguration.serverAddress else { 49 | return .badConfiguration 50 | } 51 | 52 | let endpoint: NWEndpoint 53 | 54 | if let colonRange = serverAddress.rangeOfCharacter(from: CharacterSet(charactersIn: ":"), options: [], range: nil) { 55 | // The server is specified in the configuration as :. 56 | 57 | let hostname = serverAddress.substring(with: serverAddress.startIndex...size, maximumLength: MemoryLayout.size) { data, error in 95 | if let readError = error { 96 | simpleTunnelLog("Got an error on the tunnel connection: \(readError)") 97 | self.closeTunnelWithError(readError as NSError?) 98 | return 99 | } 100 | 101 | let lengthData = data 102 | 103 | guard lengthData?.count == MemoryLayout.size else { 104 | simpleTunnelLog("Length data length (\(String(describing: lengthData?.count))) != sizeof(UInt32) (\(MemoryLayout.size)") 105 | self.closeTunnelWithError(SimpleTunnelError.internalError as NSError) 106 | return 107 | } 108 | 109 | var totalLength: UInt32 = 0 110 | (lengthData! as NSData).getBytes(&totalLength, length: MemoryLayout.size) 111 | 112 | if totalLength > UInt32(Tunnel.maximumMessageSize) { 113 | simpleTunnelLog("Got a length that is too big: \(totalLength)") 114 | self.closeTunnelWithError(SimpleTunnelError.internalError as NSError) 115 | return 116 | } 117 | 118 | totalLength -= UInt32(MemoryLayout.size) 119 | 120 | // Second, read the packet payload. 121 | targetConnection.readMinimumLength(Int(totalLength), maximumLength: Int(totalLength)) { data, error in 122 | if let payloadReadError = error { 123 | simpleTunnelLog("Got an error on the tunnel connection: \(payloadReadError)") 124 | self.closeTunnelWithError(payloadReadError as NSError?) 125 | return 126 | } 127 | 128 | let payloadData = data 129 | 130 | guard payloadData?.count == Int(totalLength) else { 131 | simpleTunnelLog("Payload data length (\(String(describing: payloadData?.count))) != payload length (\(totalLength)") 132 | self.closeTunnelWithError(SimpleTunnelError.internalError as NSError) 133 | return 134 | } 135 | 136 | _ = self.handlePacket(payloadData!) 137 | 138 | self.readNextPacket() 139 | } 140 | } 141 | } 142 | 143 | /// Send a message to the tunnel server. 144 | open func sendMessage(_ messageProperties: [String: AnyObject], completionHandler: @escaping (NSError?) -> Void) { 145 | guard let messageData = serializeMessage(messageProperties) else { 146 | completionHandler(SimpleTunnelError.internalError as NSError) 147 | return 148 | } 149 | 150 | connection?.write(messageData, completionHandler: completionHandler as! (Error?) -> Void) 151 | } 152 | 153 | // MARK: NSObject 154 | 155 | /// Handle changes to the tunnel connection state. 156 | open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { 157 | guard keyPath == "state" && context?.assumingMemoryBound(to: Optional.self).pointee == connection else { 158 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 159 | return 160 | } 161 | 162 | simpleTunnelLog("Tunnel connection state changed to \(connection!.state)") 163 | 164 | switch connection!.state { 165 | case .connected: 166 | if let remoteAddress = self.connection!.remoteAddress as? NWHostEndpoint { 167 | remoteHost = remoteAddress.hostname 168 | } 169 | 170 | // Start reading messages from the tunnel connection. 171 | readNextPacket() 172 | 173 | // Let the delegate know that the tunnel is open 174 | delegate?.tunnelDidOpen(self) 175 | 176 | case .disconnected: 177 | closeTunnelWithError(connection!.error as NSError?) 178 | 179 | case .cancelled: 180 | connection!.removeObserver(self, forKeyPath:"state", context:&connection) 181 | connection = nil 182 | delegate?.tunnelDidClose(self) 183 | 184 | default: 185 | break 186 | } 187 | } 188 | 189 | // MARK: Tunnel 190 | 191 | /// Close the tunnel. 192 | override open func closeTunnel() { 193 | super.closeTunnel() 194 | // Close the tunnel connection. 195 | if let TCPConnection = connection { 196 | TCPConnection.cancel() 197 | } 198 | 199 | } 200 | 201 | /// Write data to the tunnel connection. 202 | override func writeDataToTunnel(_ data: Data, startingAtOffset: Int) -> Int { 203 | connection?.write(data) { error in 204 | if error != nil { 205 | self.closeTunnelWithError(error as NSError?) 206 | } 207 | } 208 | return data.count 209 | } 210 | 211 | /// Handle a message received from the tunnel server. 212 | override func handleMessage(_ commandType: TunnelCommand, properties: [String: AnyObject], connection: Connection?) -> Bool { 213 | var success = true 214 | 215 | switch commandType { 216 | case .openResult: 217 | // A logical connection was opened successfully. 218 | guard let targetConnection = connection, 219 | let resultCodeNumber = properties[TunnelMessageKey.ResultCode.rawValue] as? Int, 220 | let resultCode = TunnelConnectionOpenResult(rawValue: resultCodeNumber) 221 | else 222 | { 223 | success = false 224 | break 225 | } 226 | 227 | targetConnection.handleOpenCompleted(resultCode, properties:properties as [NSObject : AnyObject]) 228 | 229 | case .fetchConfiguration: 230 | guard let configuration = properties[TunnelMessageKey.Configuration.rawValue] as? [String: AnyObject] 231 | else { break } 232 | 233 | delegate?.tunnelDidSendConfiguration(self, configuration: configuration) 234 | 235 | default: 236 | simpleTunnelLog("Tunnel received an invalid command") 237 | success = false 238 | } 239 | return success 240 | } 241 | 242 | /// Send a FetchConfiguration message on the tunnel connection. 243 | open func sendFetchConfiguation() { 244 | let properties = createMessagePropertiesForConnection(0, commandType: .fetchConfiguration) 245 | if !sendMessage(properties) { 246 | simpleTunnelLog("Failed to send a fetch configuration message") 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /SimpleTunnelServices/Connection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the Connection class. The Connection class is an abstract base class that handles a single flow of network data in the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | 10 | import Foundation 11 | 12 | /// The directions in which a flow can be closed for further data. 13 | public enum TunnelConnectionCloseDirection: Int, CustomStringConvertible { 14 | case none = 1 15 | case read = 2 16 | case write = 3 17 | case all = 4 18 | 19 | public var description: String { 20 | switch self { 21 | case .none: return "none" 22 | case .read: return "reads" 23 | case .write: return "writes" 24 | case .all: return "reads and writes" 25 | } 26 | } 27 | } 28 | 29 | /// The results of opening a connection. 30 | public enum TunnelConnectionOpenResult: Int { 31 | case success = 0 32 | case invalidParam 33 | case noSuchHost 34 | case refused 35 | case timeout 36 | case internalError 37 | } 38 | 39 | /// A logical connection (or flow) of network data in the SimpleTunnel protocol. 40 | open class Connection: NSObject { 41 | 42 | // MARK: Properties 43 | 44 | /// The connection identifier. 45 | open let identifier: Int 46 | 47 | /// The tunnel that contains the connection. 48 | open var tunnel: Tunnel? 49 | 50 | /// The list of data that needs to be written to the connection when possible. 51 | let savedData = SavedData() 52 | 53 | /// The direction(s) in which the connection is closed. 54 | var currentCloseDirection = TunnelConnectionCloseDirection.none 55 | 56 | /// Indicates if the tunnel is being used by this connection exclusively. 57 | let isExclusiveTunnel: Bool 58 | 59 | /// Indicates if the connection cannot be read from. 60 | open var isClosedForRead: Bool { 61 | return currentCloseDirection != .none && currentCloseDirection != .write 62 | } 63 | 64 | /// Indicates if the connection cannot be written to. 65 | open var isClosedForWrite: Bool { 66 | return currentCloseDirection != .none && currentCloseDirection != .read 67 | } 68 | 69 | /// Indicates if the connection is fully closed. 70 | open var isClosedCompletely: Bool { 71 | return currentCloseDirection == .all 72 | } 73 | 74 | // MARK: Initializers 75 | 76 | public init(connectionIdentifier: Int, parentTunnel: Tunnel) { 77 | tunnel = parentTunnel 78 | identifier = connectionIdentifier 79 | isExclusiveTunnel = false 80 | super.init() 81 | if let t = tunnel { 82 | // Add this connection to the tunnel's set of connections. 83 | t.addConnection(self) 84 | } 85 | 86 | } 87 | 88 | public init(connectionIdentifier: Int) { 89 | isExclusiveTunnel = true 90 | identifier = connectionIdentifier 91 | } 92 | 93 | // MARK: Interface 94 | 95 | /// Set a new tunnel for the connection. 96 | func setNewTunnel(_ newTunnel: Tunnel) { 97 | tunnel = newTunnel 98 | if let t = tunnel { 99 | t.addConnection(self) 100 | } 101 | } 102 | 103 | /// Close the connection. 104 | open func closeConnection(_ direction: TunnelConnectionCloseDirection) { 105 | if direction != .none && direction != currentCloseDirection { 106 | currentCloseDirection = .all 107 | } 108 | else { 109 | currentCloseDirection = direction 110 | } 111 | 112 | guard let currentTunnel = tunnel , currentCloseDirection == .all else { return } 113 | 114 | if isExclusiveTunnel { 115 | currentTunnel.closeTunnel() 116 | } 117 | else { 118 | currentTunnel.dropConnection(self) 119 | tunnel = nil 120 | } 121 | } 122 | 123 | /// Abort the connection. 124 | open func abort(_ error: Int = 0) { 125 | savedData.clear() 126 | } 127 | 128 | /// Send data on the connection. 129 | open func sendData(_ data: Data) { 130 | } 131 | 132 | /// Send data and the destination host and port on the connection. 133 | open func sendDataWithEndPoint(_ data: Data, host: String, port: Int) { 134 | } 135 | 136 | /// Send a list of IP packets and their associated protocols on the connection. 137 | open func sendPackets(_ packets: [Data], protocols: [NSNumber]) { 138 | } 139 | 140 | /// Send an indication to the remote end of the connection that the caller will not be reading any more data from the connection for a while. 141 | open func suspend() { 142 | } 143 | 144 | /// Send an indication to the remote end of the connection that the caller is going to start reading more data from the connection. 145 | open func resume() { 146 | } 147 | 148 | /// Handle the "open completed" message sent by the SimpleTunnel server. 149 | open func handleOpenCompleted(_ resultCode: TunnelConnectionOpenResult, properties: [NSObject: AnyObject]) { 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /SimpleTunnelServices/FilterUtilities.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the FilterUtilities class. FilterUtilities objects contain functions and data that is used by both the SimpleTunnel UI and the SimpleTunnel content filter providers. 7 | */ 8 | 9 | import Foundation 10 | import NetworkExtension 11 | 12 | /// Content Filter actions. 13 | public enum FilterRuleAction : Int, CustomStringConvertible { 14 | case block = 1 15 | case allow = 2 16 | case needMoreRulesAndBlock = 3 17 | case needMoreRulesAndAllow = 4 18 | case needMoreRulesFromDataAndBlock = 5 19 | case needMoreRulesFromDataAndAllow = 6 20 | case examineData = 7 21 | case redirectToSafeURL = 8 22 | case remediate = 9 23 | 24 | public var description: String { 25 | switch self { 26 | case .block: return "Block" 27 | case .examineData: return "Examine Data" 28 | case .needMoreRulesAndAllow: return "Ask for more rules, then allow" 29 | case .needMoreRulesAndBlock: return "Ask for more rules, then block" 30 | case .needMoreRulesFromDataAndAllow: return "Ask for more rules, examine data, then allow" 31 | case .needMoreRulesFromDataAndBlock: return "Ask for more rules, examine data, then block" 32 | case .redirectToSafeURL: return "Redirect" 33 | case .remediate: return "Remediate" 34 | case .allow: return "Allow" 35 | } 36 | } 37 | } 38 | 39 | /// A class containing utility properties and functions for Content Filtering. 40 | open class FilterUtilities { 41 | 42 | // MARK: Properties 43 | 44 | /// A reference to the SimpleTunnel user defaults. 45 | open static let defaults = UserDefaults(suiteName: "group.com.example.apple-samplecode.SimpleTunnel") 46 | 47 | // MARK: Initializers 48 | 49 | /// Get rule parameters for a flow from the SimpleTunnel user defaults. 50 | open class func getRule(_ flow: NEFilterFlow) -> (FilterRuleAction, String, [String: AnyObject]) { 51 | let hostname = FilterUtilities.getFlowHostname(flow) 52 | 53 | guard !hostname.isEmpty else { return (.allow, hostname, [:]) } 54 | 55 | guard let hostNameRule = (defaults?.object(forKey: "rules") as AnyObject).object(forKey: hostname) as? [String: AnyObject] else { 56 | simpleTunnelLog("\(hostname) is set for NO RULES") 57 | return (.allow, hostname, [:]) 58 | } 59 | 60 | guard let ruleTypeInt = hostNameRule["kRule"] as? Int, 61 | let ruleType = FilterRuleAction(rawValue: ruleTypeInt) 62 | else { return (.allow, hostname, [:]) } 63 | 64 | return (ruleType, hostname, hostNameRule) 65 | } 66 | 67 | /// Get the hostname from a browser flow. 68 | open class func getFlowHostname(_ flow: NEFilterFlow) -> String { 69 | guard let browserFlow : NEFilterBrowserFlow = flow as? NEFilterBrowserFlow, 70 | let url = browserFlow.url, 71 | let hostname = url.host 72 | , flow is NEFilterBrowserFlow 73 | else { return "" } 74 | return hostname 75 | } 76 | 77 | /// Download a fresh set of rules from the rules server. 78 | open class func fetchRulesFromServer(_ serverAddress: String?) { 79 | simpleTunnelLog("fetch rules called") 80 | 81 | guard serverAddress != nil else { return } 82 | simpleTunnelLog("Fetching rules from \(String(describing: serverAddress))") 83 | 84 | guard let infoURL = URL(string: "http://\(serverAddress!)/rules/") else { return } 85 | simpleTunnelLog("Rules url is \(infoURL)") 86 | 87 | let content: String 88 | do { 89 | content = try String(contentsOf: infoURL, encoding: String.Encoding.utf8) 90 | } 91 | catch { 92 | simpleTunnelLog("Failed to fetch the rules from \(infoURL)") 93 | return 94 | } 95 | 96 | let contentArray = content.components(separatedBy: "
") 97 | simpleTunnelLog("Content array is \(contentArray)") 98 | var urlRules = [String: [String: AnyObject]]() 99 | 100 | for rule in contentArray { 101 | if rule.isEmpty { 102 | continue 103 | } 104 | let ruleArray = rule.components(separatedBy: " ") 105 | 106 | guard !ruleArray.isEmpty else { continue } 107 | 108 | var redirectKey = "SafeYes" 109 | var remediateKey = "Remediate1" 110 | var remediateButtonKey = "RemediateButton1" 111 | var actionString = "9" 112 | 113 | let urlString = ruleArray[0] 114 | let ruleArrayCount = ruleArray.count 115 | 116 | if ruleArrayCount > 1 { 117 | actionString = ruleArray[1] 118 | } 119 | if ruleArrayCount > 2 { 120 | redirectKey = ruleArray[2] 121 | } 122 | if ruleArrayCount > 3 { 123 | remediateKey = ruleArray[3] 124 | } 125 | if ruleArrayCount > 4 { 126 | remediateButtonKey = ruleArray[4] 127 | } 128 | 129 | 130 | urlRules[urlString] = [ 131 | "kRule" : actionString as AnyObject, 132 | "kRedirectKey" : redirectKey as AnyObject, 133 | "kRemediateKey" : remediateKey as AnyObject, 134 | "kRemediateButtonKey" : remediateButtonKey as AnyObject, 135 | ] 136 | } 137 | defaults?.setValue(urlRules, forKey:"rules") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /SimpleTunnelServices/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SimpleTunnelServices/SimpleTunnelServices.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file is the main header file for the SimpleTunnelServices module. 7 | */ 8 | 9 | @import Foundation; 10 | 11 | //! Project version number for SimpleTunnelServices. 12 | FOUNDATION_EXPORT double SimpleTunnelServicesVersionNumber; 13 | 14 | //! Project version string for SimpleTunnelServices. 15 | FOUNDATION_EXPORT const unsigned char SimpleTunnelServicesVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import -------------------------------------------------------------------------------- /SimpleTunnelServices/util.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains some utility classes and functions used by various parts of the SimpleTunnel project. 7 | */ 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | /// SimpleTunnel errors 13 | public enum SimpleTunnelError: Error { 14 | case badConfiguration 15 | case badConnection 16 | case internalError 17 | } 18 | 19 | /// A queue of blobs of data 20 | class SavedData { 21 | 22 | // MARK: Properties 23 | 24 | /// Each item in the list contains a data blob and an offset (in bytes) within the data blob of the data that is yet to be written. 25 | var chain = [(data: Data, offset: Int)]() 26 | 27 | /// A convenience property to determine if the list is empty. 28 | var isEmpty: Bool { 29 | return chain.isEmpty 30 | } 31 | 32 | // MARK: Interface 33 | 34 | /// Add a data blob and offset to the end of the list. 35 | func append(_ data: Data, offset: Int) { 36 | chain.append(data: data, offset: offset) 37 | } 38 | 39 | /// Write as much of the data in the list as possible to a stream 40 | func writeToStream(_ stream: OutputStream) -> Bool { 41 | var result = true 42 | var stopIndex: Int? 43 | 44 | for (chainIndex, record) in chain.enumerated() { 45 | let written = writeData(record.data, toStream: stream, startingAtOffset:record.offset) 46 | if written < 0 { 47 | result = false 48 | break 49 | } 50 | if written < (record.data.count - record.offset) { 51 | // Failed to write all of the remaining data in this blob, update the offset. 52 | chain[chainIndex] = (record.data, record.offset + written) 53 | stopIndex = chainIndex 54 | break 55 | } 56 | } 57 | 58 | if let removeEnd = stopIndex { 59 | // We did not write all of the data, remove what was written. 60 | if removeEnd > 0 { 61 | chain.removeSubrange(0...size) 95 | sin6.sin6_family = sa_family_t(AF_INET6) 96 | sin6.sin6_port = in_port_t(0) 97 | sin6.sin6_addr = in6addr_any 98 | sin6.sin6_scope_id = __uint32_t(0) 99 | sin6.sin6_flowinfo = __uint32_t(0) 100 | } 101 | 102 | convenience init(otherAddress: SocketAddress6) { 103 | self.init() 104 | sin6 = otherAddress.sin6 105 | } 106 | 107 | /// Set the IPv6 address from a string. 108 | func setFromString(_ str: String) -> Bool { 109 | return str.withCString({ cs in inet_pton(AF_INET6, cs, &sin6.sin6_addr) }) == 1 110 | } 111 | 112 | /// Set the port. 113 | func setPort(_ port: Int) { 114 | sin6.sin6_port = in_port_t(UInt16(port).bigEndian) 115 | } 116 | } 117 | 118 | /// An object containing a sockaddr_in structure. 119 | class SocketAddress { 120 | 121 | // MARK: Properties 122 | 123 | /// The sockaddr_in structure. 124 | var sin: sockaddr_in 125 | 126 | /// The IPv4 address in string form. 127 | var stringValue: String? { 128 | return withUnsafePointer(to: &sin) { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { saToString($0) } } 129 | } 130 | 131 | // MARK: Initializers 132 | 133 | init() { 134 | sin = sockaddr_in(sin_len:__uint8_t(MemoryLayout.size), sin_family:sa_family_t(AF_INET), sin_port:in_port_t(0), sin_addr:in_addr(s_addr: 0), sin_zero:(Int8(0), Int8(0), Int8(0), Int8(0), Int8(0), Int8(0), Int8(0), Int8(0))) 135 | } 136 | 137 | convenience init(otherAddress: SocketAddress) { 138 | self.init() 139 | sin = otherAddress.sin 140 | } 141 | 142 | /// Set the IPv4 address from a string. 143 | func setFromString(_ str: String) -> Bool { 144 | return str.withCString({ cs in inet_pton(AF_INET, cs, &sin.sin_addr) }) == 1 145 | } 146 | 147 | /// Set the port. 148 | func setPort(_ port: Int) { 149 | sin.sin_port = in_port_t(UInt16(port).bigEndian) 150 | } 151 | 152 | /// Increment the address by a given amount. 153 | func increment(_ amount: UInt32) { 154 | let networkAddress = sin.sin_addr.s_addr.byteSwapped + amount 155 | sin.sin_addr.s_addr = networkAddress.byteSwapped 156 | } 157 | 158 | /// Get the difference between this address and another address. 159 | func difference(_ otherAddress: SocketAddress) -> Int64 { 160 | return Int64(sin.sin_addr.s_addr.byteSwapped) - Int64(otherAddress.sin.sin_addr.s_addr.byteSwapped) 161 | } 162 | } 163 | 164 | // MARK: Utility Functions 165 | 166 | /// Convert a sockaddr structure to a string. 167 | func saToString(_ sa: UnsafePointer) -> String? { 168 | var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 169 | var portBuffer = [CChar](repeating: 0, count: Int(NI_MAXSERV)) 170 | 171 | guard getnameinfo(sa, socklen_t(sa.pointee.sa_len), &hostBuffer, socklen_t(hostBuffer.count), &portBuffer, socklen_t(portBuffer.count), NI_NUMERICHOST | NI_NUMERICSERV) == 0 172 | else { return nil } 173 | 174 | return String(cString: hostBuffer) 175 | } 176 | 177 | /// Write a blob of data to a stream starting from a particular offset. 178 | func writeData(_ data: Data, toStream stream: OutputStream, startingAtOffset offset: Int) -> Int { 179 | var written = 0 180 | var currentOffset = offset 181 | while stream.hasSpaceAvailable && currentOffset < data.count { 182 | 183 | let writeResult = stream.write((data as NSData).bytes.bindMemory(to: UInt8.self, capacity: data.count) + currentOffset, maxLength: data.count - currentOffset) 184 | guard writeResult >= 0 else { return writeResult } 185 | 186 | written += writeResult 187 | currentOffset += writeResult 188 | } 189 | 190 | return written 191 | } 192 | 193 | /// Create a SimpleTunnel protocol message dictionary. 194 | public func createMessagePropertiesForConnection(_ connectionIdentifier: Int, commandType: TunnelCommand, extraProperties: [String: AnyObject] = [:]) -> [String: AnyObject] { 195 | // Start out with the "extra properties" that the caller specified. 196 | var properties = extraProperties 197 | 198 | // Add in the standard properties common to all messages. 199 | properties[TunnelMessageKey.Identifier.rawValue] = connectionIdentifier as AnyObject? 200 | properties[TunnelMessageKey.Command.rawValue] = commandType.rawValue as AnyObject? 201 | 202 | return properties 203 | } 204 | 205 | /// Keys in the tunnel server configuration plist. 206 | public enum SettingsKey: String { 207 | case IPv4 = "IPv4" 208 | case DNS = "DNS" 209 | case Proxies = "Proxies" 210 | case Pool = "Pool" 211 | case StartAddress = "StartAddress" 212 | case EndAddress = "EndAddress" 213 | case Servers = "Servers" 214 | case SearchDomains = "SearchDomains" 215 | case Address = "Address" 216 | case Netmask = "Netmask" 217 | case Routes = "Routes" 218 | } 219 | 220 | /// Get a value from a plist given a list of keys. 221 | public func getValueFromPlist(_ plist: [NSObject: AnyObject], keyArray: [SettingsKey]) -> AnyObject? { 222 | var subPlist = plist 223 | for (index, key) in keyArray.enumerated() { 224 | if index == keyArray.count - 1 { 225 | return subPlist[key.rawValue as NSString] 226 | } 227 | else if let subSubPlist = subPlist[key.rawValue as NSString] as? [NSObject: AnyObject] { 228 | subPlist = subSubPlist 229 | } 230 | else { 231 | break 232 | } 233 | } 234 | 235 | return nil 236 | } 237 | 238 | /// Create a new range by incrementing the start of the given range by a given ammount. 239 | func rangeByMovingStartOfRange(_ range: Range, byCount: Int) -> CountableRange { 240 | return (range.lowerBound + byCount)..= 0 else { 56 | simpleTunnelLog("start address (\(startAddress)) is greater than end address (\(endAddress))") 57 | return 58 | } 59 | 60 | baseAddress.sin = start.sin 61 | size = UInt64(difference) 62 | inUseMask = [Bool](repeating: false, count: Int(size)) 63 | } 64 | 65 | /// Allocate an address from the pool. 66 | func allocateAddress() -> String? { 67 | var result: String? 68 | 69 | queue.sync() { 70 | let address = SocketAddress(otherAddress: self.baseAddress) 71 | 72 | // Look for an address that is not currently allocated 73 | for (index, inUse) in self.inUseMask.enumerated() { 74 | if !inUse { 75 | address.increment(UInt32(index)) 76 | self.inUseMask[index] = true 77 | result = address.stringValue 78 | break 79 | } 80 | } 81 | } 82 | 83 | simpleTunnelLog("Allocated address \(String(describing: result))") 84 | return result 85 | } 86 | 87 | /// Deallocate an address in the pool. 88 | func deallocateAddress(addrString: String) { 89 | queue.sync() { 90 | let address = SocketAddress() 91 | 92 | guard address.setFromString(addrString) else { return } 93 | 94 | let difference = address.difference(self.baseAddress) 95 | if difference >= 0 && difference < Int64(self.inUseMask.count) { 96 | self.inUseMask[Int(difference)] = false 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tunnel_server/ServerConfiguration.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ServerConfiguration class. The ServerConfiguration class is used to parse the SimpleTunnel server configuration. 7 | */ 8 | 9 | import Foundation 10 | import SystemConfiguration 11 | 12 | /// An object containing configuration settings for the SimpleTunnel server. 13 | class ServerConfiguration { 14 | 15 | // MARK: Properties 16 | 17 | /// A dictionary containing configuration parameters. 18 | var configuration: [String: AnyObject] 19 | 20 | /// A pool of IP addresses to allocate to clients. 21 | var addressPool: AddressPool? 22 | 23 | // MARK: Initializers 24 | 25 | init() { 26 | configuration = [String: AnyObject]() 27 | addressPool = nil 28 | } 29 | 30 | // MARK: Interface 31 | 32 | /// Read the configuration settings from a plist on disk. 33 | func loadFromFileAtPath(path: String) -> Bool { 34 | 35 | guard let fileStream = InputStream(fileAtPath: path) else { 36 | simpleTunnelLog("Failed to open \(path) for reading") 37 | return false 38 | } 39 | 40 | fileStream.open() 41 | 42 | var newConfiguration: [String: AnyObject] 43 | do { 44 | newConfiguration = try PropertyListSerialization.propertyList(with: fileStream, options: .mutableContainers, format: nil) as! [String: AnyObject] 45 | } 46 | catch { 47 | simpleTunnelLog("Failed to read the configuration from \(path): \(error)") 48 | return false 49 | } 50 | 51 | guard let startAddress = getValueFromPlist(newConfiguration as [NSObject : AnyObject], keyArray: [.IPv4, .Pool, .StartAddress]) as? String else { 52 | simpleTunnelLog("Missing v4 start address") 53 | return false 54 | } 55 | guard let endAddress = getValueFromPlist(newConfiguration as [NSObject : AnyObject], keyArray: [.IPv4, .Pool, .EndAddress]) as? String else { 56 | simpleTunnelLog("Missing v4 end address") 57 | return false 58 | } 59 | 60 | addressPool = AddressPool(startAddress: startAddress, endAddress: endAddress) 61 | 62 | // The configuration dictionary gets sent to clients as the tunnel settings dictionary. Remove the IP pool parameters. 63 | if let value = newConfiguration[SettingsKey.IPv4.rawValue] as? [NSObject: AnyObject] { 64 | var IPv4Dictionary = value 65 | 66 | IPv4Dictionary.removeValue(forKey: SettingsKey.Pool.rawValue as NSObject) 67 | newConfiguration[SettingsKey.IPv4.rawValue] = IPv4Dictionary as AnyObject 68 | } 69 | 70 | if !newConfiguration.keys.contains(where: { $0 == SettingsKey.DNS.rawValue }) { 71 | // The configuration does not specify any DNS configuration, so get the current system default resolver. 72 | let (DNSServers, DNSSearchDomains) = ServerConfiguration.copyDNSConfigurationFromSystem() 73 | 74 | newConfiguration[SettingsKey.DNS.rawValue] = [ 75 | SettingsKey.Servers.rawValue: DNSServers, 76 | SettingsKey.SearchDomains.rawValue: DNSSearchDomains 77 | ] as AnyObject 78 | } 79 | 80 | configuration = newConfiguration 81 | 82 | return true 83 | } 84 | 85 | /// Copy the default resolver configuration from the system on which the server is running. 86 | class func copyDNSConfigurationFromSystem() -> ([String], [String]) { 87 | let globalDNSKey = SCDynamicStoreKeyCreateNetworkGlobalEntity(kCFAllocatorDefault, kSCDynamicStoreDomainState, kSCEntNetDNS) 88 | var DNSServers = [String]() 89 | var DNSSearchDomains = [String]() 90 | 91 | // The default resolver configuration can be obtained from State:/Network/Global/DNS in the dynamic store. 92 | 93 | if let globalDNS = SCDynamicStoreCopyValue(nil, globalDNSKey) as? [NSObject: AnyObject], 94 | let servers = globalDNS[kSCPropNetDNSServerAddresses as NSObject] as? [String] 95 | { 96 | if let searchDomains = globalDNS[kSCPropNetDNSSearchDomains as NSObject] as? [String] { 97 | DNSSearchDomains = searchDomains 98 | } 99 | DNSServers = servers 100 | } 101 | 102 | return (DNSServers, DNSSearchDomains) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tunnel_server/ServerConnection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ServerConnection class. The ServerConnection class encapsulates and decapsulates a stream of network data in the server side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | 11 | /// An object representing the server side of a logical flow of TCP network data in the SimpleTunnel tunneling protocol. 12 | class ServerConnection: Connection, StreamDelegate { 13 | 14 | // MARK: Properties 15 | 16 | /// The stream used to read network data from the connection. 17 | var readStream: InputStream? 18 | 19 | /// The stream used to write network data to the connection. 20 | var writeStream: OutputStream? 21 | 22 | // MARK: Interface 23 | 24 | /// Open the connection to a host and port. 25 | func open(host: String, port: Int) -> Bool { 26 | simpleTunnelLog("Connection \(identifier) connecting to \(host):\(port)") 27 | 28 | Stream.getStreamsToHost(withName: host, port: port, inputStream: &readStream, outputStream: &writeStream) 29 | 30 | guard let newReadStream = readStream, let newWriteStream = writeStream else { 31 | return false 32 | } 33 | 34 | for stream in [newReadStream, newWriteStream] { 35 | stream.delegate = self 36 | stream.open() 37 | stream.schedule(in: .main, forMode: RunLoopMode.defaultRunLoopMode) 38 | } 39 | 40 | return true 41 | } 42 | 43 | // MARK: Connection 44 | 45 | /// Close the connection. 46 | override func closeConnection(_ direction: TunnelConnectionCloseDirection) { 47 | super.closeConnection(direction) 48 | 49 | if let stream = writeStream, isClosedForWrite && savedData.isEmpty { 50 | if let error = stream.streamError { 51 | simpleTunnelLog("Connection \(identifier) write stream error: \(error)") 52 | } 53 | 54 | stream.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 55 | stream.close() 56 | stream.delegate = nil 57 | writeStream = nil 58 | } 59 | 60 | if let stream = readStream, isClosedForRead { 61 | if let error = stream.streamError { 62 | simpleTunnelLog("Connection \(identifier) read stream error: \(error)") 63 | } 64 | 65 | stream.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 66 | stream.close() 67 | stream.delegate = nil 68 | readStream = nil 69 | } 70 | } 71 | 72 | /// Abort the connection. 73 | override func abort(_ error: Int = 0) { 74 | super.abort(error) 75 | closeConnection(.all) 76 | } 77 | 78 | /// Stop reading from the connection. 79 | override func suspend() { 80 | if let stream = readStream { 81 | stream.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 82 | } 83 | } 84 | 85 | /// Start reading from the connection. 86 | override func resume() { 87 | if let stream = readStream { 88 | stream.schedule(in: .main, forMode: RunLoopMode.defaultRunLoopMode) 89 | } 90 | } 91 | 92 | /// Send data over the connection. 93 | override func sendData(_ data: Data) { 94 | guard let stream = writeStream else { return } 95 | var written = 0 96 | 97 | if savedData.isEmpty { 98 | written = writeData(data as Data, toStream: stream, startingAtOffset: 0) 99 | 100 | if written < data.count { 101 | // We could not write all of the data to the connection. Tell the client to stop reading data for this connection. 102 | stream.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 103 | tunnel?.sendSuspendForConnection(identifier) 104 | } 105 | } 106 | 107 | if written < data.count { 108 | savedData.append(data as Data, offset: written) 109 | } 110 | } 111 | 112 | // MARK: NSStreamDelegate 113 | 114 | /// Handle an event on a stream. 115 | func stream(aStream: Stream, handleEvent eventCode: Stream.Event) { 116 | switch aStream { 117 | 118 | case writeStream!: 119 | switch eventCode { 120 | case [.hasSpaceAvailable]: 121 | if !savedData.isEmpty { 122 | guard savedData.writeToStream(writeStream!) else { 123 | tunnel?.sendCloseType(.all, forConnection: identifier) 124 | abort() 125 | break 126 | } 127 | 128 | if savedData.isEmpty { 129 | writeStream?.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 130 | if isClosedForWrite { 131 | closeConnection(.write) 132 | } 133 | else { 134 | tunnel?.sendResumeForConnection(identifier) 135 | } 136 | } 137 | } 138 | else { 139 | writeStream?.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 140 | } 141 | 142 | case [.endEncountered]: 143 | tunnel?.sendCloseType(.read, forConnection: identifier) 144 | closeConnection(.write) 145 | 146 | case [.errorOccurred]: 147 | tunnel?.sendCloseType(.all, forConnection: identifier) 148 | abort() 149 | 150 | default: 151 | break 152 | } 153 | 154 | case readStream!: 155 | switch eventCode { 156 | case [.hasBytesAvailable]: 157 | if let stream = readStream { 158 | while stream.hasBytesAvailable { 159 | var readBuffer = [UInt8](repeating: 0, count: 8192) 160 | let bytesRead = stream.read(&readBuffer, maxLength: readBuffer.count) 161 | 162 | if bytesRead < 0 { 163 | abort() 164 | break 165 | } 166 | 167 | if bytesRead == 0 { 168 | simpleTunnelLog("\(identifier): got EOF, sending close") 169 | tunnel?.sendCloseType(.write, forConnection: identifier) 170 | closeConnection(.read) 171 | break 172 | } 173 | 174 | let readData = NSData(bytes: readBuffer, length: bytesRead) 175 | tunnel?.sendData(readData as Data, forConnection: identifier) 176 | } 177 | } 178 | 179 | case [.endEncountered]: 180 | tunnel?.sendCloseType(.write, forConnection: identifier) 181 | closeConnection(.read) 182 | 183 | case [.errorOccurred]: 184 | if let serverTunnel = tunnel as? ServerTunnel { 185 | serverTunnel.sendOpenResultForConnection(connectionIdentifier: identifier, resultCode: .timeout) 186 | serverTunnel.sendCloseType(.all, forConnection: identifier) 187 | abort() 188 | } 189 | 190 | case [.openCompleted]: 191 | if let serverTunnel = tunnel as? ServerTunnel { 192 | serverTunnel.sendOpenResultForConnection(connectionIdentifier: identifier, resultCode: .success) 193 | } 194 | 195 | default: 196 | break 197 | } 198 | default: 199 | break 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tunnel_server/ServerTunnel.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ServerTunnel class. The ServerTunnel class implements the server side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | import SystemConfiguration 11 | 12 | /// The server-side implementation of the SimpleTunnel protocol. 13 | class ServerTunnel: Tunnel, TunnelDelegate, StreamDelegate { 14 | 15 | // MARK: Properties 16 | 17 | /// The stream used to read data from the tunnel TCP connection. 18 | var readStream: InputStream? 19 | 20 | /// The stream used to write data to the tunnel TCP connection. 21 | var writeStream: OutputStream? 22 | 23 | /// A buffer where the data for the current packet is accumulated. 24 | let packetBuffer = NSMutableData() 25 | 26 | /// The number of bytes remaining to be read for the current packet. 27 | var packetBytesRemaining = 0 28 | 29 | /// The server configuration parameters. 30 | static var configuration = ServerConfiguration() 31 | 32 | /// The delegate for the network service published by the server. 33 | static var serviceDelegate = ServerDelegate() 34 | 35 | // MARK: Initializers 36 | 37 | init(newReadStream: InputStream, newWriteStream: OutputStream) { 38 | super.init() 39 | delegate = self 40 | 41 | for stream in [newReadStream, newWriteStream] { 42 | stream.delegate = self 43 | stream.open() 44 | stream.schedule(in: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode) 45 | } 46 | readStream = newReadStream 47 | writeStream = newWriteStream 48 | } 49 | 50 | // MARK: Class Methods 51 | 52 | /// Start the network service. 53 | class func startListeningOnPort(port: Int32) -> NetService { 54 | let service = NetService(domain:Tunnel.serviceDomain, type:Tunnel.serviceType, name: "", port: port) 55 | 56 | simpleTunnelLog("Starting network service on port \(port)") 57 | 58 | service.delegate = ServerTunnel.serviceDelegate 59 | service.publish(options: NetService.Options.listenForConnections) 60 | service.schedule(in: .main, forMode: RunLoopMode.defaultRunLoopMode) 61 | 62 | return service 63 | } 64 | 65 | /// Load the configuration from disk. 66 | class func initializeWithConfigurationFile(path: String) -> Bool { 67 | return ServerTunnel.configuration.loadFromFileAtPath(path: path) 68 | } 69 | 70 | // MARK: Interface 71 | 72 | /// Handle a bytes available event on the read stream. 73 | func handleBytesAvailable() -> Bool { 74 | 75 | guard let stream = readStream else { return false } 76 | var readBuffer = [UInt8](repeating: 0, count: Tunnel.maximumMessageSize) 77 | 78 | repeat { 79 | var toRead = 0 80 | var bytesRead = 0 81 | 82 | if packetBytesRemaining == 0 { 83 | // Currently reading the total length of the packet. 84 | toRead = MemoryLayout.size - packetBuffer.length 85 | } 86 | else { 87 | // Currently reading the packet payload. 88 | toRead = packetBytesRemaining > readBuffer.count ? readBuffer.count : packetBytesRemaining 89 | } 90 | 91 | bytesRead = stream.read(&readBuffer, maxLength: toRead) 92 | 93 | guard bytesRead > 0 else { 94 | return false 95 | } 96 | 97 | packetBuffer.append(readBuffer, length: bytesRead) 98 | 99 | if packetBytesRemaining == 0 { 100 | // Reading the total length, see if the 4 length bytes have been received. 101 | if packetBuffer.length == MemoryLayout.size { 102 | var totalLength: UInt32 = 0 103 | packetBuffer.getBytes(&totalLength, length: MemoryLayout.size(ofValue: totalLength)) 104 | 105 | guard totalLength <= UInt32(Tunnel.maximumMessageSize) else { return false } 106 | 107 | // Compute the length of the payload. 108 | packetBytesRemaining = Int(totalLength) - MemoryLayout.size(ofValue: totalLength) 109 | packetBuffer.length = 0 110 | } 111 | } 112 | else { 113 | // Read a portion of the payload. 114 | packetBytesRemaining -= bytesRead 115 | if packetBytesRemaining == 0 { 116 | // The entire packet has been received, process it. 117 | if !handlePacket(packetBuffer as Data) { 118 | return false 119 | } 120 | packetBuffer.length = 0 121 | } 122 | } 123 | } while stream.hasBytesAvailable 124 | 125 | return true 126 | } 127 | 128 | /// Send an "Open Result" message to the client. 129 | func sendOpenResultForConnection(connectionIdentifier: Int, resultCode: TunnelConnectionOpenResult) { 130 | let properties = createMessagePropertiesForConnection(connectionIdentifier, commandType: .openResult, extraProperties:[ 131 | TunnelMessageKey.ResultCode.rawValue: resultCode.rawValue as AnyObject 132 | ]) 133 | 134 | if !sendMessage(properties) { 135 | simpleTunnelLog("Failed to send an open result for connection \(connectionIdentifier)") 136 | } 137 | } 138 | 139 | /// Handle a "Connection Open" message received from the client. 140 | func handleConnectionOpen(properties: [String: AnyObject]) { 141 | guard let connectionIdentifier = properties[TunnelMessageKey.Identifier.rawValue] as? Int, 142 | let tunnelLayerNumber = properties[TunnelMessageKey.TunnelType.rawValue] as? Int, 143 | let tunnelLayer = TunnelLayer(rawValue: tunnelLayerNumber) 144 | else { return } 145 | 146 | switch tunnelLayer { 147 | case .app: 148 | 149 | guard let flowKindNumber = properties[TunnelMessageKey.AppProxyFlowType.rawValue] as? Int, 150 | let flowKind = AppProxyFlowKind(rawValue: flowKindNumber) 151 | else { break } 152 | 153 | switch flowKind { 154 | case .tcp: 155 | guard let host = properties[TunnelMessageKey.Host.rawValue] as? String, 156 | let port = properties[TunnelMessageKey.Port.rawValue] as? NSNumber 157 | else { break } 158 | let newConnection = ServerConnection(connectionIdentifier: connectionIdentifier, parentTunnel: self) 159 | guard newConnection.open(host: host, port: port.intValue) else { 160 | newConnection.closeConnection(.all) 161 | break 162 | } 163 | 164 | case .udp: 165 | let _ = UDPServerConnection(connectionIdentifier: connectionIdentifier, parentTunnel: self) 166 | sendOpenResultForConnection(connectionIdentifier: connectionIdentifier, resultCode: .success) 167 | } 168 | 169 | case .ip: 170 | let newConnection = ServerTunnelConnection(connectionIdentifier: connectionIdentifier, parentTunnel: self) 171 | guard newConnection.open() else { 172 | newConnection.closeConnection(.all) 173 | break 174 | } 175 | } 176 | } 177 | 178 | // MARK: NSStreamDelegate 179 | 180 | /// Handle a stream event. 181 | func stream(aStream: Stream, handleEvent eventCode: Stream.Event) { 182 | switch aStream { 183 | 184 | case writeStream!: 185 | switch eventCode { 186 | case [.hasSpaceAvailable]: 187 | // Send any buffered data. 188 | if !savedData.isEmpty { 189 | guard savedData.writeToStream(writeStream!) else { 190 | closeTunnel() 191 | delegate?.tunnelDidClose(self) 192 | break 193 | } 194 | 195 | if savedData.isEmpty { 196 | for connection in connections.values { 197 | connection.resume() 198 | } 199 | } 200 | } 201 | 202 | case [.errorOccurred]: 203 | closeTunnel() 204 | delegate?.tunnelDidClose(self) 205 | 206 | default: 207 | break 208 | } 209 | 210 | case readStream!: 211 | var needCloseTunnel = false 212 | switch eventCode { 213 | case [.hasBytesAvailable]: 214 | needCloseTunnel = !handleBytesAvailable() 215 | 216 | case [.openCompleted]: 217 | delegate?.tunnelDidOpen(self) 218 | 219 | case [.errorOccurred], [.endEncountered]: 220 | needCloseTunnel = true 221 | 222 | default: 223 | break 224 | } 225 | 226 | if needCloseTunnel { 227 | closeTunnel() 228 | delegate?.tunnelDidClose(self) 229 | } 230 | 231 | default: 232 | break 233 | } 234 | 235 | } 236 | 237 | // MARK: Tunnel 238 | 239 | /// Close the tunnel. 240 | override func closeTunnel() { 241 | 242 | if let stream = readStream { 243 | if let error = stream.streamError { 244 | simpleTunnelLog("Tunnel read stream error: \(error)") 245 | } 246 | 247 | let socketData = CFReadStreamCopyProperty(stream, CFStreamPropertyKey.socketNativeHandle) as? NSData 248 | 249 | stream.remove(from: RunLoop.main, forMode: RunLoopMode.defaultRunLoopMode) 250 | stream.close() 251 | stream.delegate = nil 252 | readStream = nil 253 | 254 | if let data = socketData { 255 | var socket: CFSocketNativeHandle = 0 256 | data.getBytes(&socket, length: MemoryLayout.size(ofValue: socket)) 257 | close(socket) 258 | } 259 | } 260 | 261 | if let stream = writeStream { 262 | if let error = stream.streamError { 263 | simpleTunnelLog("Tunnel write stream error: \(error)") 264 | } 265 | 266 | stream.remove(from: .main, forMode: RunLoopMode.defaultRunLoopMode) 267 | stream.close() 268 | stream.delegate = nil 269 | } 270 | 271 | super.closeTunnel() 272 | } 273 | 274 | /// Handle a message received from the client. 275 | override func handleMessage(_ commandType: TunnelCommand, properties: [String: AnyObject], connection: Connection?) -> Bool { 276 | switch commandType { 277 | case .open: 278 | handleConnectionOpen(properties: properties) 279 | 280 | case .fetchConfiguration: 281 | var personalized = ServerTunnel.configuration.configuration 282 | personalized.removeValue(forKey: SettingsKey.IPv4.rawValue) 283 | let messageProperties = createMessagePropertiesForConnection(0, commandType: .fetchConfiguration, extraProperties: [TunnelMessageKey.Configuration.rawValue: personalized as AnyObject]) 284 | let _ = sendMessage(messageProperties) 285 | 286 | default: 287 | break 288 | } 289 | return true 290 | } 291 | 292 | /// Write data to the tunnel connection. 293 | override func writeDataToTunnel(_ data: Data, startingAtOffset: Int) -> Int { 294 | guard let stream = writeStream else { return -1 } 295 | return writeData(data as Data, toStream: stream, startingAtOffset:startingAtOffset) 296 | } 297 | 298 | // MARK: TunnelDelegate 299 | 300 | /// Handle the "tunnel open" event. 301 | func tunnelDidOpen(_ targetTunnel: Tunnel) { 302 | } 303 | 304 | /// Handle the "tunnel closed" event. 305 | func tunnelDidClose(_ targetTunnel: Tunnel) { 306 | } 307 | 308 | /// Handle the "tunnel did send configuration" event. 309 | func tunnelDidSendConfiguration(_ targetTunnel: Tunnel, configuration: [String : AnyObject]) { 310 | } 311 | } 312 | 313 | /// An object that servers as the delegate for the network service published by the server. 314 | class ServerDelegate : NSObject, NetServiceDelegate { 315 | 316 | // MARK: NSNetServiceDelegate 317 | 318 | /// Handle the "failed to publish" event. 319 | func netService(sender: NetService, didNotPublish errorDict: [String : NSNumber]) { 320 | simpleTunnelLog("Failed to publish network service") 321 | exit(1) 322 | } 323 | 324 | /// Handle the "published" event. 325 | func netServiceDidPublish(sender: NetService) { 326 | simpleTunnelLog("Network service published successfully") 327 | } 328 | 329 | /// Handle the "new connection" event. 330 | func netService(_ sender: NetService, didAcceptConnectionWith inputStream: InputStream, outputStream: OutputStream) { 331 | simpleTunnelLog("Accepted a new connection") 332 | _ = ServerTunnel(newReadStream: inputStream, newWriteStream: outputStream) 333 | } 334 | 335 | /// Handle the "stopped" event. 336 | func netServiceDidStop(sender: NetService) { 337 | simpleTunnelLog("Network service stopped") 338 | exit(0) 339 | } 340 | } 341 | 342 | -------------------------------------------------------------------------------- /tunnel_server/ServerTunnelConnection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the ServerTunnelConnection class. The ServerTunnelConnection class handles the encapsulation and decapsulation of IP packets in the server side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | /// An object that provides a bridge between a logical flow of packets in the SimpleTunnel protocol and a UTUN interface. 13 | class ServerTunnelConnection: Connection { 14 | 15 | // MARK: Properties 16 | 17 | /// The virtual address of the tunnel. 18 | var tunnelAddress: String? 19 | 20 | /// The name of the UTUN interface. 21 | var utunName: String? 22 | 23 | /// A dispatch source for the UTUN interface socket. 24 | var utunSource: DispatchSourceProtocol? 25 | 26 | /// A flag indicating if reads from the UTUN interface are suspended. 27 | var isSuspended = false 28 | 29 | // MARK: Interface 30 | 31 | /// Send an "open result" message with optionally the tunnel settings. 32 | func sendOpenResult(result: TunnelConnectionOpenResult, extraProperties: [String: AnyObject] = [:]) { 33 | guard let serverTunnel = tunnel else { return } 34 | 35 | var resultProperties = extraProperties 36 | resultProperties[TunnelMessageKey.ResultCode.rawValue] = result.rawValue as AnyObject 37 | 38 | let properties = createMessagePropertiesForConnection(identifier, commandType: .openResult, extraProperties: resultProperties) 39 | 40 | let _ = serverTunnel.sendMessage(properties) 41 | } 42 | 43 | /// "Open" the connection by setting up the UTUN interface. 44 | func open() -> Bool { 45 | 46 | // Allocate the tunnel virtual address. 47 | guard let address = ServerTunnel.configuration.addressPool?.allocateAddress() else { 48 | simpleTunnelLog("Failed to allocate a tunnel address") 49 | sendOpenResult(result: .refused) 50 | return false 51 | } 52 | 53 | // Create the virtual interface and assign the address. 54 | guard setupVirtualInterface(address: address) else { 55 | simpleTunnelLog("Failed to set up the virtual interface") 56 | ServerTunnel.configuration.addressPool?.deallocateAddress(addrString: address) 57 | sendOpenResult(result: .internalError) 58 | return false 59 | } 60 | 61 | tunnelAddress = address 62 | 63 | var response = [String: AnyObject]() 64 | 65 | // Create a copy of the configuration, so that it can be personalized with the tunnel virtual interface. 66 | var personalized = ServerTunnel.configuration.configuration 67 | guard let IPv4Dictionary = personalized[SettingsKey.IPv4.rawValue] as? [NSObject: AnyObject] else { 68 | simpleTunnelLog("No IPv4 Settings available") 69 | sendOpenResult(result: .internalError) 70 | return false 71 | } 72 | 73 | // Set up the "IPv4" sub-dictionary to contain the tunne virtual address and network mask. 74 | var newIPv4Dictionary = IPv4Dictionary 75 | newIPv4Dictionary[SettingsKey.Address.rawValue as NSObject] = tunnelAddress as AnyObject 76 | newIPv4Dictionary[SettingsKey.Netmask.rawValue as NSObject] = "255.255.255.255" as AnyObject 77 | personalized[SettingsKey.IPv4.rawValue] = newIPv4Dictionary as AnyObject 78 | response[TunnelMessageKey.Configuration.rawValue] = personalized as AnyObject 79 | 80 | // Send the personalized configuration along with the "open result" message. 81 | sendOpenResult(result: .success, extraProperties: response) 82 | 83 | return true 84 | } 85 | 86 | /// Create a UTUN interface. 87 | func createTUNInterface() -> Int32 { 88 | 89 | let utunSocket = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL) 90 | guard utunSocket >= 0 else { 91 | simpleTunnelLog("Failed to open a kernel control socket") 92 | return -1 93 | } 94 | 95 | let controlIdentifier = getUTUNControlIdentifier(utunSocket) 96 | guard controlIdentifier > 0 else { 97 | simpleTunnelLog("Failed to get the control ID for the utun kernel control") 98 | close(utunSocket) 99 | return -1 100 | } 101 | 102 | // Connect the socket to the UTUN kernel control. 103 | var socketAddressControl = sockaddr_ctl(sc_len: UInt8(MemoryLayout.size), sc_family: UInt8(AF_SYSTEM), ss_sysaddr: UInt16(AF_SYS_CONTROL), sc_id: controlIdentifier, sc_unit: 0, sc_reserved: (0, 0, 0, 0, 0)) 104 | 105 | let connectResult = withUnsafePointer(to: &socketAddressControl) { 106 | $0.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { 107 | connect(utunSocket, $0, socklen_t(MemoryLayout.size(ofValue: socketAddressControl))) 108 | }) 109 | } 110 | 111 | if let errorString = String(utf8String: strerror(errno)), connectResult < 0 { 112 | simpleTunnelLog("Failed to create a utun interface: \(errorString)") 113 | close(utunSocket) 114 | return -1 115 | } 116 | 117 | return utunSocket 118 | } 119 | 120 | /// Get the name of a UTUN interface the associated socket. 121 | func getTUNInterfaceName(utunSocket: Int32) -> String? { 122 | var buffer = [Int8](repeating: 0, count: Int(IFNAMSIZ)) 123 | var bufferSize: socklen_t = socklen_t(buffer.count) 124 | let resultCode = getsockopt(utunSocket, SYSPROTO_CONTROL, getUTUNNameOption(), &buffer, &bufferSize) 125 | if let errorString = String(utf8String: strerror(errno)), resultCode < 0 { 126 | simpleTunnelLog("getsockopt failed while getting the utun interface name: \(errorString)") 127 | return nil 128 | } 129 | return String (utf8String: &buffer) 130 | } 131 | 132 | /// Set up the UTUN interface, start reading packets. 133 | func setupVirtualInterface(address: String) -> Bool { 134 | let utunSocket = createTUNInterface() 135 | guard let interfaceName = getTUNInterfaceName(utunSocket: utunSocket), utunSocket >= 0 && 136 | setUTUNAddress(interfaceName, address) 137 | else { return false } 138 | 139 | startTunnelSource(utunSocket: utunSocket) 140 | utunName = interfaceName 141 | return true 142 | } 143 | 144 | /// Read packets from the UTUN interface. 145 | func readPackets() { 146 | guard let source = utunSource else { return } 147 | var packets = [NSData]() 148 | var protocols = [NSNumber]() 149 | 150 | // We use a 2-element iovec list. The first iovec points to the protocol number of the packet, the second iovec points to the buffer where the packet should be read. 151 | var buffer = [UInt8](repeating:0, count: Tunnel.packetSize) 152 | var protocolNumber: UInt32 = 0 153 | var iovecList = [ iovec(iov_base: &protocolNumber, iov_len: MemoryLayout.size(ofValue: protocolNumber)), iovec(iov_base: &buffer, iov_len: buffer.count) ] 154 | let iovecListPointer = UnsafeBufferPointer(start: &iovecList, count: iovecList.count) 155 | let utunSocket = Int32(source.handle) 156 | 157 | repeat { 158 | let readCount = readv(utunSocket, iovecListPointer.baseAddress, Int32(iovecListPointer.count)) 159 | 160 | guard readCount > 0 || errno == EAGAIN else { 161 | if let errorString = String(utf8String: strerror(errno)), readCount < 0 { 162 | simpleTunnelLog("Got an error on the utun socket: \(errorString)") 163 | } 164 | source.cancel() 165 | break 166 | } 167 | 168 | guard readCount > MemoryLayout.size(ofValue: protocolNumber) else { break } 169 | 170 | if protocolNumber.littleEndian == protocolNumber { 171 | protocolNumber = protocolNumber.byteSwapped 172 | } 173 | protocols.append(NSNumber(value: protocolNumber)) 174 | packets.append(NSData(bytes: &buffer, length: readCount - MemoryLayout.size(ofValue: protocolNumber))) 175 | 176 | // Buffer up packets so that we can include multiple packets per message. Once we reach a per-message maximum send a "packets" message. 177 | if packets.count == Tunnel.maximumPacketsPerMessage { 178 | tunnel?.sendPackets(packets as [Data], protocols: protocols, forConnection: identifier) 179 | packets = [NSData]() 180 | protocols = [NSNumber]() 181 | if isSuspended { break } // If the entire message could not be sent and the connection is suspended, stop reading packets. 182 | } 183 | } while true 184 | 185 | // If there are unsent packets left over, send them now. 186 | if packets.count > 0 { 187 | tunnel?.sendPackets(packets as [Data], protocols: protocols, forConnection: identifier) 188 | } 189 | } 190 | 191 | /// Start reading packets from the UTUN interface. 192 | func startTunnelSource(utunSocket: Int32) { 193 | guard setSocketNonBlocking(utunSocket) else { return } 194 | 195 | let newSource : DispatchSourceRead = DispatchSource.makeReadSource(fileDescriptor: utunSocket, queue: DispatchQueue.main) 196 | newSource.setCancelHandler { 197 | close(utunSocket) 198 | return 199 | } 200 | 201 | newSource.setEventHandler { 202 | self.readPackets() 203 | } 204 | 205 | newSource.resume() 206 | utunSource = newSource 207 | } 208 | 209 | // MARK: Connection 210 | 211 | /// Abort the connection. 212 | override func abort(_ error: Int = 0) { 213 | super.abort(error) 214 | closeConnection(.all) 215 | } 216 | 217 | /// Close the connection. 218 | override func closeConnection(_ direction: TunnelConnectionCloseDirection) { 219 | super.closeConnection(direction) 220 | 221 | if currentCloseDirection == .all { 222 | if utunSource != nil { 223 | utunSource!.cancel() 224 | } 225 | // De-allocate the address. 226 | if tunnelAddress != nil { 227 | ServerTunnel.configuration.addressPool?.deallocateAddress(addrString: tunnelAddress!) 228 | } 229 | utunName = nil 230 | } 231 | } 232 | 233 | /// Stop reading packets from the UTUN interface. 234 | override func suspend() { 235 | isSuspended = true 236 | if let source = utunSource { 237 | source.suspend() 238 | } 239 | } 240 | 241 | /// Resume reading packets from the UTUN interface. 242 | override func resume() { 243 | isSuspended = false 244 | if let source = utunSource { 245 | source.resume() 246 | readPackets() 247 | } 248 | } 249 | 250 | /// Write packets and associated protocols to the UTUN interface. 251 | override func sendPackets(_ packets: [Data], protocols: [NSNumber]) { 252 | guard let source = utunSource else { return } 253 | let utunSocket = Int32(source.handle) 254 | 255 | for (index, packet) in packets.enumerated() { 256 | guard index < protocols.count else { break } 257 | 258 | var protocolNumber = protocols[index].uint32Value.bigEndian 259 | 260 | packet.withUnsafeBytes({ (pointer : UnsafePointer) in 261 | let buffer = UnsafeMutableRawPointer(mutating: pointer) 262 | var iovecList = [ iovec(iov_base: &protocolNumber, iov_len: MemoryLayout.size(ofValue: protocolNumber)), iovec(iov_base: buffer, iov_len: packet.count) ] 263 | 264 | let writeCount = writev(utunSocket, &iovecList, Int32(iovecList.count)) 265 | if writeCount < 0 { 266 | if let errorString = String(utf8String: strerror(errno)) { 267 | simpleTunnelLog("Got an error while writing to utun: \(errorString)") 268 | } 269 | } 270 | else if writeCount < packet.count + MemoryLayout.size(ofValue: protocolNumber) { 271 | simpleTunnelLog("Wrote \(writeCount) bytes of a \(packet.count) byte packet to utun") 272 | } 273 | }) 274 | 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tunnel_server/ServerUtils.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file declares utility Objective-C functions used by the SimpleTunnel server. 7 | */ 8 | 9 | #ifndef ServerUtils_h 10 | #define ServerUtils_h 11 | 12 | /// Get the identifier for the UTUN interface. 13 | UInt32 getUTUNControlIdentifier(int socket); 14 | 15 | /// Setup a socket as non-blocking. 16 | BOOL setUTUNAddress(NSString *ifname, NSString *address); 17 | 18 | /// Set the IP address on a UTUN interface. 19 | int getUTUNNameOption(void); 20 | 21 | /// Get value of the UTUN iterface name socket option. 22 | BOOL setSocketNonBlocking(int socket); 23 | 24 | #endif /* ServerUtils_h */ 25 | -------------------------------------------------------------------------------- /tunnel_server/ServerUtils.m: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains utility Objective-C code used by the SimpleTunnel server. 7 | */ 8 | 9 | @import Foundation; 10 | @import Darwin; 11 | 12 | #import 13 | #import "tunnel_server-Bridging-Header.h" 14 | 15 | UInt32 16 | getUTUNControlIdentifier(int socket) 17 | { 18 | struct ctl_info kernelControlInfo; 19 | 20 | bzero(&kernelControlInfo, sizeof(kernelControlInfo)); 21 | strlcpy(kernelControlInfo.ctl_name, UTUN_CONTROL_NAME, sizeof(kernelControlInfo.ctl_name)); 22 | 23 | if (ioctl(socket, CTLIOCGINFO, &kernelControlInfo)) { 24 | printf("ioctl failed on kernel control socket: %s\n", strerror(errno)); 25 | return 0; 26 | } 27 | 28 | return kernelControlInfo.ctl_id; 29 | } 30 | 31 | BOOL 32 | setSocketNonBlocking(int socket) 33 | { 34 | int currentFlags = fcntl(socket, F_GETFL); 35 | if (currentFlags < 0) { 36 | printf("fcntl(F_GETFL) failed: %s\n", strerror(errno)); 37 | return NO; 38 | } 39 | 40 | currentFlags |= O_NONBLOCK; 41 | 42 | if (fcntl(socket, F_SETFL, currentFlags) < 0) { 43 | printf("fcntl(F_SETFL) failed: %s\n", strerror(errno)); 44 | return NO; 45 | } 46 | 47 | return YES; 48 | } 49 | 50 | BOOL 51 | setUTUNAddress(NSString *interfaceName, NSString *addressString) 52 | { 53 | struct in_addr address; 54 | 55 | if (inet_pton(AF_INET, [addressString UTF8String], &address) == 1) { 56 | struct ifaliasreq interfaceAliasRequest __attribute__ ((aligned (4))); 57 | struct in_addr mask = { 0xffffffff }; 58 | int socketDescriptor = socket(AF_INET, SOCK_DGRAM, 0); 59 | 60 | if (socketDescriptor < 0) { 61 | printf("Failed to create a DGRAM socket: %s\n", strerror(errno)); 62 | return NO; 63 | } 64 | 65 | memset(&interfaceAliasRequest, 0, sizeof(interfaceAliasRequest)); 66 | 67 | strlcpy(interfaceAliasRequest.ifra_name, [interfaceName UTF8String], sizeof(interfaceAliasRequest.ifra_name)); 68 | 69 | interfaceAliasRequest.ifra_addr.sa_family = AF_INET; 70 | interfaceAliasRequest.ifra_addr.sa_len = sizeof(struct sockaddr_in); 71 | memcpy(&((struct sockaddr_in *)&interfaceAliasRequest.ifra_addr)->sin_addr, &address, sizeof(address)); 72 | 73 | interfaceAliasRequest.ifra_broadaddr.sa_family = AF_INET; 74 | interfaceAliasRequest.ifra_broadaddr.sa_len = sizeof(struct sockaddr_in); 75 | memcpy(&((struct sockaddr_in *)&interfaceAliasRequest.ifra_broadaddr)->sin_addr, &address, sizeof(address)); 76 | 77 | interfaceAliasRequest.ifra_mask.sa_family = AF_INET; 78 | interfaceAliasRequest.ifra_mask.sa_len = sizeof(struct sockaddr_in); 79 | memcpy(&((struct sockaddr_in *)&interfaceAliasRequest.ifra_mask)->sin_addr, &mask, sizeof(mask)); 80 | 81 | if (ioctl(socketDescriptor, SIOCAIFADDR, &interfaceAliasRequest) < 0) { 82 | printf("Failed to set the address of %s interface address to %s: %s\n", [interfaceName UTF8String], [addressString UTF8String], strerror(errno)); 83 | close(socketDescriptor); 84 | return NO; 85 | } 86 | 87 | close(socketDescriptor); 88 | } 89 | 90 | return YES; 91 | } 92 | 93 | int 94 | getUTUNNameOption(void) 95 | { 96 | return UTUN_OPT_IFNAME; 97 | } -------------------------------------------------------------------------------- /tunnel_server/UDPServerConnection.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the UDPServerConnection class. The UDPServerConnection class handles the encapsulation and decapsulation of datagrams in the server side of the SimpleTunnel tunneling protocol. 7 | */ 8 | 9 | import Foundation 10 | import Darwin 11 | 12 | /// An object representing the server side of a logical flow of UDP network data in the SimpleTunnel tunneling protocol. 13 | class UDPServerConnection: Connection { 14 | 15 | // MARK: Properties 16 | 17 | /// The address family of the UDP socket. 18 | var addressFamily: Int32 = AF_UNSPEC 19 | 20 | /// A dispatch source for reading data from the UDP socket. 21 | var responseSource: DispatchSourceProtocol? 22 | 23 | // MARK: Initializers 24 | 25 | override init(connectionIdentifier: Int, parentTunnel: Tunnel) { 26 | super.init(connectionIdentifier: connectionIdentifier, parentTunnel: parentTunnel) 27 | } 28 | 29 | deinit { 30 | if let source = responseSource { 31 | source.cancel() 32 | } 33 | } 34 | 35 | // MARK: Interface 36 | 37 | /// Convert a sockaddr structure into an IP address string and port. 38 | func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer) -> (host: String, port: Int)? { 39 | let socketAddress = UnsafePointer(socketAddressPointer).pointee 40 | 41 | switch Int32(socketAddress.sa_family) { 42 | case AF_INET: 43 | 44 | return socketAddressPointer.withMemoryRebound(to: sockaddr_in.self, capacity: MemoryLayout.size) { (socketAddressInetPointer: UnsafePointer) -> (host: String, port: Int) in 45 | let length = Int(INET_ADDRSTRLEN) + 2 46 | var buffer = [CChar](repeating: 0, count: length) 47 | var sin_addr = socketAddressInetPointer.pointee.sin_addr 48 | let hostCString = inet_ntop(AF_INET, &sin_addr, &buffer, socklen_t(length)) 49 | let port = Int(UInt16(socketAddressInetPointer.pointee.sin_port).byteSwapped) 50 | return (String(cString: hostCString!), port) 51 | } 52 | case AF_INET6: 53 | 54 | return socketAddressPointer.withMemoryRebound(to: sockaddr_in6.self, capacity: MemoryLayout.size) { (socketAddressInet6Pointer : UnsafePointer) -> (host: String, port: Int) in 55 | let length = Int(INET6_ADDRSTRLEN) + 2 56 | var buffer = [CChar](repeating: 0, count: length) 57 | var sin6_addr = socketAddressInet6Pointer.pointee.sin6_addr 58 | let hostCString = inet_ntop(AF_INET6, &sin6_addr, &buffer, socklen_t(length)) 59 | let port = Int(UInt16(socketAddressInet6Pointer.pointee.sin6_port).byteSwapped) 60 | return (String(cString: hostCString!), port) 61 | } 62 | 63 | default: 64 | return nil 65 | } 66 | } 67 | 68 | /// Create a UDP socket 69 | func createSocketWithAddressFamilyFromAddress(address: String) -> Bool { 70 | var sin = sockaddr_in() 71 | var sin6 = sockaddr_in6() 72 | var newSocket: Int32 = -1 73 | 74 | if address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 { 75 | // IPv6 peer. 76 | newSocket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP) 77 | addressFamily = AF_INET6 78 | } 79 | else if address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 { 80 | // IPv4 peer. 81 | newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) 82 | addressFamily = AF_INET 83 | } 84 | 85 | guard newSocket > 0 else { return false } 86 | 87 | let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: DispatchQueue.main) 88 | 89 | newResponseSource.setCancelHandler { 90 | simpleTunnelLog("closing udp socket for connection \(self.identifier)") 91 | let UDPSocket = Int32(newResponseSource.handle) 92 | close(UDPSocket) 93 | } 94 | 95 | newResponseSource.setEventHandler { 96 | guard let source = self.responseSource else { return } 97 | 98 | var socketAddress = sockaddr_storage() 99 | var socketAddressLength = socklen_t(MemoryLayout.size) 100 | let response = [UInt8](repeating: 0, count: 4096) 101 | let UDPSocket = Int32(source.handle) 102 | 103 | let bytesRead = withUnsafeMutablePointer(to: &socketAddress, { 104 | $0.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { 105 | recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, $0, &socketAddressLength) 106 | }) 107 | }) 108 | 109 | guard bytesRead >= 0 else { 110 | if let errorString = String(utf8String: strerror(errno)) { 111 | simpleTunnelLog("recvfrom failed: \(errorString)") 112 | } 113 | self.closeConnection(.all) 114 | return 115 | } 116 | 117 | guard bytesRead > 0 else { 118 | simpleTunnelLog("recvfrom returned EOF") 119 | self.closeConnection(.all) 120 | return 121 | } 122 | 123 | guard let endpoint = withUnsafePointer(to: &socketAddress, { 124 | $0.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { 125 | return self.getEndpointFromSocketAddress(socketAddressPointer: $0) 126 | }) 127 | }) 128 | else { 129 | simpleTunnelLog("Failed to get the address and port from the socket address received from recvfrom") 130 | self.closeConnection(.all) 131 | return 132 | } 133 | 134 | let responseDatagram = Data(bytes: UnsafeRawPointer(response), count: bytesRead) 135 | simpleTunnelLog("UDP connection id \(self.identifier) received = \(bytesRead) bytes from host = \(endpoint.host) port = \(endpoint.port)") 136 | self.tunnel?.sendDataWithEndPoint(responseDatagram, forConnection: self.identifier, host: endpoint.host, port: endpoint.port) 137 | } 138 | 139 | newResponseSource.resume() 140 | responseSource = newResponseSource 141 | 142 | return true 143 | } 144 | 145 | /// Send a datagram to a given host and port. 146 | override func sendDataWithEndPoint(_ data: Data, host: String, port: Int) { 147 | 148 | if responseSource == nil { 149 | guard createSocketWithAddressFamilyFromAddress(address: host) else { 150 | simpleTunnelLog("UDP ServerConnection initialization failed.") 151 | return 152 | } 153 | } 154 | 155 | guard let source = responseSource else { return } 156 | let UDPSocket = Int32(source.handle) 157 | let sent: Int 158 | 159 | switch addressFamily { 160 | case AF_INET: 161 | let serverAddress = SocketAddress() 162 | guard serverAddress.setFromString(host) else { 163 | simpleTunnelLog("Failed to convert \(host) into an IPv4 address") 164 | return 165 | } 166 | serverAddress.setPort(port) 167 | 168 | sent = withUnsafePointer(to: &serverAddress.sin, { 169 | $0.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { (sockaddrPointer) in 170 | data.withUnsafeBytes({ (bytes) in 171 | sendto(UDPSocket, bytes, data.count, 0, sockaddrPointer, socklen_t(serverAddress.sin.sin_len)) 172 | }) 173 | }) 174 | }) 175 | 176 | case AF_INET6: 177 | let serverAddress = SocketAddress6() 178 | guard serverAddress.setFromString(host) else { 179 | simpleTunnelLog("Failed to convert \(host) into an IPv6 address") 180 | return 181 | } 182 | serverAddress.setPort(port) 183 | 184 | sent = withUnsafePointer(to: &serverAddress.sin6, { 185 | $0.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size, { (sockaddrPointer) in 186 | data.withUnsafeBytes({ (bytes) in 187 | sendto(UDPSocket, bytes, data.count, 0, sockaddrPointer, socklen_t(serverAddress.sin6.sin6_len)) 188 | }) 189 | }) 190 | }) 191 | 192 | default: 193 | return 194 | } 195 | 196 | guard sent > 0 else { 197 | if let errorString = String(utf8String: strerror(errno)) { 198 | simpleTunnelLog("UDP connection id \(identifier) failed to send data to host = \(host) port \(port). error = \(errorString)") 199 | } 200 | closeConnection(.all) 201 | return 202 | } 203 | 204 | if sent == data.count { 205 | // Success 206 | simpleTunnelLog("UDP connection id \(identifier) sent \(data.count) bytes to host = \(host) port \(port)") 207 | } 208 | } 209 | 210 | /// Close the connection. 211 | override func closeConnection(_ direction: TunnelConnectionCloseDirection) { 212 | super.closeConnection(direction) 213 | 214 | if let source = responseSource, isClosedForWrite && isClosedForRead { 215 | source.cancel() 216 | responseSource = nil 217 | } 218 | } 219 | } 220 | 221 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /tunnel_server/config.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IPv4 6 | 7 | Pool 8 | 9 | EndAddress 10 | 192.168.2.10 11 | StartAddress 12 | 192.168.2.2 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tunnel_server/main.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file contains the main code for the SimpleTunnel server. 7 | */ 8 | 9 | import Foundation 10 | 11 | /// Dispatch source to catch and handle SIGINT 12 | let interruptSignalSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: DispatchQueue.main) 13 | 14 | /// Dispatch source to catch and handle SIGTERM 15 | let termSignalSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: DispatchQueue.main) 16 | 17 | /// Basic sanity check of the parameters. 18 | if ProcessInfo().arguments.count < 3 { 19 | print("Usage: \(ProcessInfo().arguments[0]) ") 20 | exit(1) 21 | } 22 | 23 | func ignore(_: Int32) { 24 | } 25 | signal(SIGTERM, ignore) 26 | signal(SIGINT, ignore) 27 | 28 | let portString = ProcessInfo().arguments[1] 29 | let configurationPath = ProcessInfo().arguments[2] 30 | let networkService: NetService 31 | 32 | // Initialize the server. 33 | 34 | if !ServerTunnel.initializeWithConfigurationFile(path: configurationPath) { 35 | exit(1) 36 | } 37 | 38 | if let portNumber = Int(portString) { 39 | networkService = ServerTunnel.startListeningOnPort(port: Int32(portNumber)) 40 | } 41 | else { 42 | print("Invalid port: \(String(describing: portString))") 43 | exit(1) 44 | } 45 | 46 | // Set up signal handling. 47 | 48 | interruptSignalSource.setEventHandler { 49 | networkService.stop() 50 | return 51 | } 52 | interruptSignalSource.resume() 53 | 54 | termSignalSource.setEventHandler { 55 | networkService.stop() 56 | return 57 | } 58 | termSignalSource.resume() 59 | 60 | RunLoop.main.run() 61 | -------------------------------------------------------------------------------- /tunnel_server/tunnel_server-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2016 Apple Inc. All Rights Reserved. 3 | See LICENSE.txt for this sample’s licensing information 4 | 5 | Abstract: 6 | This file exposes utility Objective-C functions to Swift. 7 | */ 8 | 9 | @import Foundation; 10 | 11 | #import "ServerUtils.h" 12 | --------------------------------------------------------------------------------