├── .gitignore
├── AppProxy
├── AppProxy.entitlements
├── AppProxyClientTunnel.swift
├── 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 | build/
4 | *.pbxuser
5 | !default.pbxuser
6 | *.mode1v3
7 | !default.mode1v3
8 | *.mode2v3
9 | !default.mode2v3
10 | *.perspectivev3
11 | !default.perspectivev3
12 | xcuserdata
13 | *.xccheckout
14 | *.moved-aside
15 | DerivedData
16 | *.hmap
17 | *.ipa
18 | *.xcuserstate
19 |
20 | # CocoaPods
21 | #
22 | # We recommend against adding the Pods directory to your .gitignore. However
23 | # you should judge for yourself, the pros and cons are mentioned at:
24 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
25 | #
26 | # Pods/
27 |
28 | # Carthage
29 | #
30 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
31 | # Carthage/Checkouts
32 |
33 | Carthage/Build
34 |
--------------------------------------------------------------------------------
/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/AppProxyClientTunnel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppProxyClientTunnel.swift
3 | // SimpleTunnel
4 | //
5 | // Created by Paresh Sawant on 5/6/15.
6 | // Copyright © 2015 Apple Inc. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import NetworkExtension
11 | import SimpleTunnelServices
12 |
13 | public class AppProxyClientTunnel : ClientTunnel {
14 | public func sendDataToTunnel(queue: dispatch_queue_t, data: NSData, startingAtOffset: Int) -> Int {
15 | self.connection?.write(data) {
16 | error in
17 | if error != nil {
18 | self.closeTunnelWithError(error)
19 |
20 | }
21 | dispatch_resume(queue)
22 | }
23 | dispatch_suspend(queue)
24 | //return super.writeDataToTunnel(data: data, startingAtOffset: startingAtOffset)
25 | return data.length
26 | }
27 | }
--------------------------------------------------------------------------------
/AppProxy/AppProxyProvider.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 startProxyWithOptions(options: [String : AnyObject]?, completionHandler: (NSError?) -> 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 stopProxyWithReason(reason: NEProviderStopReason, completionHandler: () -> 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 |
78 | // Call the pending start completion handler
79 | pendingStartCompletion?(nil)
80 | pendingStartCompletion = nil
81 | }
82 |
83 | /// Handle the event of the tunnel being fully disconnected.
84 | func tunnelDidClose(targetTunnel: Tunnel) {
85 |
86 | // Call the appropriate completion handler depending on the current pending tunnel operation.
87 | if pendingStartCompletion != nil {
88 | pendingStartCompletion?(tunnel?.lastError)
89 | pendingStartCompletion = nil
90 | }
91 | else if pendingStopCompletion != nil {
92 | pendingStopCompletion?()
93 | pendingStopCompletion = nil
94 | }
95 | else {
96 | // No completion handler, so cancel the proxy.
97 | cancelProxyWithError(tunnel?.lastError)
98 | }
99 | tunnel = nil
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/AppProxy/ClientAppProxyConnection.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 Apple Inc. All Rights Reserved.
3 | See LICENSE.txt for this sample’s licensing information
4 |
5 | Abstract:
6 | This file contains the ClientAppProxyTCPConnection and ClientAppProxyUDPConnection classes. The ClientAppProxyTCPConnection class handles the encapsulation and decapsulation of a stream of application network data in the client side of the SimpleTunnel tunneling protocol. The ClientAppProxyUDPConnection class handles the encapsulation and decapsulation of a sequence of datagrams containing application network data in the client side of the SimpleTunnel tunneling protocol.
7 | */
8 |
9 | import Foundation
10 | import SimpleTunnelServices
11 | import NetworkExtension
12 |
13 | /// An object representing the client side of a logical flow of network data in the SimpleTunnel tunneling protocol.
14 | class ClientAppProxyConnection : Connection {
15 |
16 | // MARK: Properties
17 |
18 | /// The NEAppProxyFlow object corresponding to this connection.
19 | let appProxyFlow: NEAppProxyFlow
20 |
21 | /// A dispatch queue used to regulate the sending of the connection's data through the tunnel connection.
22 | lazy var queue: dispatch_queue_t = dispatch_queue_create("ClientConnection Handle Data queue", nil)
23 |
24 | // MARK: Initializers
25 |
26 | init(tunnel: ClientTunnel, flow: NEAppProxyFlow) {
27 | appProxyFlow = flow
28 | super.init(connectionIdentifier: flow.hash, parentTunnel: tunnel)
29 | }
30 |
31 | // MARK: Interface
32 |
33 | /// Send an "Open" message to the SimpleTunnel server, to begin the process of establishing a flow of data in the SimpleTunnel protocol.
34 | func open() {
35 | open([:])
36 | }
37 |
38 | /// Send an "Open" message to the SimpleTunnel server, to begin the process of establishing a flow of data in the SimpleTunnel protocol.
39 | func open(extraProperties: [String: AnyObject]) {
40 | guard let clientTunnel = tunnel as? ClientTunnel else {
41 | // Close the NEAppProxyFlow.
42 | let error: SimpleTunnelError = .BadConnection
43 | appProxyFlow.closeReadWithError(error as NSError)
44 | appProxyFlow.closeWriteWithError(error as NSError)
45 | return
46 | }
47 |
48 | let properties = createMessagePropertiesForConnection(identifier, commandType:.Open, extraProperties:extraProperties)
49 |
50 | clientTunnel.sendMessage(properties) { error in
51 | if let error = error {
52 | // Close the NEAppProxyFlow.
53 | self.appProxyFlow.closeReadWithError(error)
54 | self.appProxyFlow.closeWriteWithError(error)
55 | }
56 | }
57 | }
58 |
59 | /// Handle the result of sending a data message to the SimpleTunnel server.
60 | func handleSendResult(error: NSError?) {
61 | }
62 |
63 | /// Handle errors that occur on the connection.
64 | func handleErrorCondition(flowError: NEAppProxyFlowError? = nil, notifyServer: Bool = true) {
65 | if notifyServer {
66 | if let clientTunnel = tunnel as? ClientTunnel {
67 | // Send a "close" message to the SimpleTunnel server.
68 | let properties = createMessagePropertiesForConnection(identifier, commandType: .Close, extraProperties:[
69 | TunnelMessageKey.CloseDirection.rawValue: TunnelConnectionCloseDirection.All.rawValue,
70 | ])
71 |
72 | clientTunnel.sendMessage(properties) { inError in
73 | self.closeConnection(.All)
74 | }
75 | }
76 | else {
77 | closeConnection(.All)
78 | }
79 | }
80 |
81 | var error: NSError?
82 | if let ferror = flowError {
83 | error = NSError(domain: NEAppProxyErrorDomain, code: ferror.rawValue, userInfo: nil)
84 | }
85 |
86 | // Close the NEAppProxyFlow.
87 | appProxyFlow.closeReadWithError(error)
88 | appProxyFlow.closeWriteWithError(error)
89 | }
90 |
91 | /// Send a "Data" message to the SimpleTunnel server.
92 | func sendDataMessage(data: NSData, extraProperties: [String: AnyObject] = [:]) {
93 | dispatch_async(queue) {
94 |
95 | // Suspend further writes to the tunnel until this write operation is completed.
96 | dispatch_suspend(self.queue)
97 |
98 | guard let clientTunnel = self.tunnel as? ClientTunnel else { return }
99 |
100 | var dataProperties = extraProperties
101 | dataProperties[TunnelMessageKey.Data.rawValue] = data
102 |
103 | let properties = createMessagePropertiesForConnection(self.identifier, commandType: .Data, extraProperties:dataProperties)
104 |
105 | clientTunnel.sendMessage(properties) { error in
106 |
107 | // Resume the queue to allow subsequent writes.
108 | dispatch_resume(self.queue)
109 |
110 | // This will schedule another read operation on the NEAppProxyFlow.
111 | self.handleSendResult(error)
112 | }
113 | }
114 | }
115 |
116 | // MARK: Connection
117 |
118 | /// Handle the "Open Completed" message received from the SimpleTunnel server for this connection.
119 | override func handleOpenCompleted(resultCode: TunnelConnectionOpenResult, properties: [NSObject: AnyObject]) {
120 | guard resultCode == .Success else {
121 | print("Failed to open \(identifier), result = \(resultCode)")
122 | handleErrorCondition(.PeerReset, notifyServer: false)
123 | return
124 | }
125 |
126 | guard let localAddress = (tunnel as? ClientTunnel)?.connection!.localAddress as? NWHostEndpoint else {
127 | print("Failed to get localAddress.")
128 | handleErrorCondition(.Internal)
129 | return
130 | }
131 |
132 | // Now that the SimpleTunnel connection is open, indicate that we are ready to handle data on the NEAppProxyFlow.
133 | appProxyFlow.openWithLocalEndpoint(localAddress) { error in
134 | self.handleSendResult(error)
135 | }
136 | }
137 |
138 | override func closeConnection(direction: TunnelConnectionCloseDirection) {
139 | super.closeConnection(direction)
140 |
141 | if isClosedForWrite {
142 | appProxyFlow.closeWriteWithError(nil)
143 | }
144 | if isClosedForRead {
145 | appProxyFlow.closeReadWithError(nil)
146 | }
147 | }
148 | }
149 |
150 | /// An object representing the client side of a logical flow of TCP network data in the SimpleTunnel tunneling protocol.
151 | class ClientAppProxyTCPConnection : ClientAppProxyConnection {
152 |
153 | // MARK: Properties
154 |
155 | /// The NEAppProxyTCPFlow object corresponding to this connection
156 | var TCPFlow: NEAppProxyTCPFlow {
157 | return (appProxyFlow as! NEAppProxyTCPFlow)
158 | }
159 |
160 | // MARK: Initializers
161 |
162 | init(tunnel: ClientTunnel, newTCPFlow: NEAppProxyTCPFlow) {
163 | super.init(tunnel: tunnel, flow: newTCPFlow)
164 | }
165 |
166 | // MARK: ClientAppProxyConnection
167 |
168 | /// Send an "Open" message to the SimpleTunnel server, to begin the process of establishing a flow of data in the SimpleTunnel protocol.
169 | override func open() {
170 | open([
171 | TunnelMessageKey.TunnelType.rawValue: TunnelLayer.App.rawValue,
172 | TunnelMessageKey.Host.rawValue: (TCPFlow.remoteEndpoint as! NWHostEndpoint).hostname,
173 | TunnelMessageKey.Port.rawValue: Int((TCPFlow.remoteEndpoint as! NWHostEndpoint).port)!,
174 | TunnelMessageKey.AppProxyFlowType.rawValue: AppProxyFlowKind.TCP.rawValue
175 | ])
176 | }
177 |
178 | /// Handle the result of sending a "Data" message to the SimpleTunnel server.
179 | override func handleSendResult(error: NSError?) {
180 | if let sendError = error {
181 | print("Failed to send Data Message to the Tunnel Server. error = \(sendError)")
182 | handleErrorCondition(.HostUnreachable)
183 | return
184 | }
185 |
186 | // Read another chunk of data from the source application.
187 | TCPFlow.readDataWithCompletionHandler { data, readError in
188 | guard let readData = data where readError == nil else {
189 | print("Failed to read data from the TCP flow. error = \(readError)")
190 | self.handleErrorCondition(.PeerReset)
191 | return
192 | }
193 |
194 | guard readData.length > 0 else {
195 | print("Received EOF on the TCP flow. Closing the flow...")
196 | self.handleErrorCondition()
197 | return
198 | }
199 |
200 | self.sendDataMessage(readData)
201 | }
202 | }
203 |
204 | /// Send data received from the SimpleTunnel server to the destination application, using the NEAppProxyTCPFlow object.
205 | override func sendData(data: NSData) {
206 | TCPFlow.writeData(data) { error in
207 | if let writeError = error {
208 | print("Failed to write data to the TCP flow. error = \(writeError)")
209 | self.handleErrorCondition()
210 | }
211 | }
212 | }
213 | }
214 |
215 | /// An object representing the client side of a logical flow of UDP network data in the SimpleTunnel tunneling protocol.
216 | class ClientAppProxyUDPConnection : ClientAppProxyConnection {
217 |
218 | // MARK: Properties
219 |
220 | /// The NEAppProxyUDPFlow object corresponding to this connection.
221 | var UDPFlow: NEAppProxyUDPFlow {
222 | return (appProxyFlow as! NEAppProxyUDPFlow)
223 | }
224 |
225 | /// The number of "Data" messages scheduled to be written to the tunnel that have not been actually sent out on the network yet.
226 | var datagramsOutstanding = 0
227 |
228 | // MARK: Initializers
229 |
230 | init(tunnel: ClientTunnel, newUDPFlow: NEAppProxyUDPFlow) {
231 | super.init(tunnel: tunnel, flow: newUDPFlow)
232 | }
233 |
234 | // MARK: ClientAppProxyConnection
235 |
236 | /// Send an "Open" message to the SimpleTunnel server, to begin the process of establishing a flow of data in the SimpleTunnel protocol.
237 | override func open() {
238 | open([
239 | TunnelMessageKey.TunnelType.rawValue: TunnelLayer.App.rawValue,
240 | TunnelMessageKey.AppProxyFlowType.rawValue: AppProxyFlowKind.UDP.rawValue
241 | ])
242 | }
243 |
244 | /// Handle the result of sending a "Data" message to the SimpleTunnel server.
245 | override func handleSendResult(error: NSError?) {
246 |
247 | if let sendError = error {
248 | print("Failed to send message to Tunnel Server. error = \(sendError)")
249 | handleErrorCondition(.HostUnreachable)
250 | return
251 | }
252 |
253 | if datagramsOutstanding > 0 {
254 | datagramsOutstanding--
255 | }
256 |
257 | // Only read more datagrams from the source application if all outstanding datagrams have been sent on the network.
258 | guard datagramsOutstanding == 0 else { return }
259 |
260 | // Read a new set of datagrams from the source application.
261 | UDPFlow.readDatagramsWithCompletionHandler { datagrams, remoteEndPoints, readError in
262 |
263 | guard let readDatagrams = datagrams,
264 | readEndpoints = remoteEndPoints
265 | where readError == nil else
266 | {
267 | print("Failed to read data from the UDP flow. error = \(readError)")
268 | self.handleErrorCondition(.PeerReset)
269 | return
270 | }
271 |
272 | guard !readDatagrams.isEmpty && readEndpoints.count == readDatagrams.count else {
273 | print("Received EOF on the UDP flow. Closing the flow...")
274 | self.handleErrorCondition()
275 | return
276 | }
277 |
278 | self.datagramsOutstanding = readDatagrams.count
279 |
280 | for (index, datagram) in readDatagrams.enumerate() {
281 | guard let endpoint = readEndpoints[index] as? NWHostEndpoint else { continue }
282 |
283 | // Send a data message to the SimpleTunnel server.
284 | self.sendDataMessage(datagram, extraProperties:[
285 | TunnelMessageKey.Host.rawValue: endpoint.hostname,
286 | TunnelMessageKey.Port.rawValue: Int(endpoint.port)!
287 | ])
288 | }
289 | }
290 | }
291 |
292 | /// Send a datagram received from the SimpleTunnel server to the destination application.
293 | override func sendDataWithEndPoint(data: NSData, host: String, port: Int) {
294 | let datagrams = [ data ]
295 | let endpoints = [ NWHostEndpoint(hostname: host, port: String(port)) ]
296 |
297 | // Send the datagram to the destination application.
298 | UDPFlow.writeDatagrams(datagrams, sentByEndpoints: endpoints) { error in
299 | if let error = error {
300 | print("Failed to write datagrams to the UDP Flow: \(error)")
301 | self.handleErrorCondition()
302 | }
303 | }
304 | }
305 | }
306 |
307 |
308 |
--------------------------------------------------------------------------------
/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) 2015 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,
22 | "kRemediationKey" : "Remediate1"
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 | print("Remediation url is \(remediationURL)")
41 |
42 | remediationMap =
43 | [
44 | NEFilterProviderRemediationMapRemediationURLs : [ "Remediate1" : remediationURL ],
45 | NEFilterProviderRemediationMapRemediationButtonTexts :
46 | [
47 | "RemediateButton1" : "Request Access",
48 | "RemediateButton2" : "\"",
49 | "RemediateButton3" : "Request Access 3",
50 | ]
51 | ]
52 |
53 | self.URLAppendStringMap = [ "SafeYes" : "safe=yes", "Adult" : "adult=yes"]
54 |
55 | print("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 observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) {
74 | if keyPath == "filterConfiguration" && context == &observerContext {
75 | print("configuration changed")
76 | updateFromConfiguration()
77 | } else {
78 | super.observeValueForKeyPath(keyPath, ofObject: 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: (NEFilterControlVerdict) -> Void) {
86 | print("Handle new flow called")
87 | var controlVerdict = NEFilterControlVerdict.updateRules()
88 | let (ruleType, hostname, _) = FilterUtilities.getRule(flow)
89 |
90 | switch ruleType {
91 | case .NeedMoreRulesAndAllow:
92 | print("\(hostname) is set to be Allowed")
93 | controlVerdict = NEFilterControlVerdict.allowVerdictWithUpdateRules(false)
94 |
95 | case .NeedMoreRulesAndBlock:
96 | print("\(hostname) is set to be blocked")
97 | controlVerdict = NEFilterControlVerdict.dropVerdictWithUpdateRules(false)
98 |
99 | default:
100 | print("\(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 | NSExtensionPrincipalClass
28 | FilterControlProvider.ControlExtension
29 | NSExtensionPointIdentifier
30 | com.apple.networkextension.filter-control
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 | NSExtensionPrincipalClass
28 | FilterDataProvider.DataExtension
29 | NSExtensionPointIdentifier
30 | com.apple.networkextension.filter-data
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Sample code project: SimpleTunnel: Customized Networking Using the NetworkExtension Framework
2 | Version: 1.0
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) 2015 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 | 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 |
--------------------------------------------------------------------------------
/PacketTunnel/PacketTunnelProvider.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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: (NSError? -> 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 startTunnelWithOptions(options: [String : NSObject]?, completionHandler: (NSError?) -> 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 stopTunnelWithReason(reason: NEProviderStopReason, completionHandler: () -> 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: NSData, completionHandler: ((NSData?) -> Void)?) {
58 | guard let messageString = NSString(data: messageData, encoding: NSUTF8StringEncoding) else {
59 | completionHandler?(nil)
60 | return
61 | }
62 |
63 | print("Got a message from the app: \(messageString)")
64 |
65 | let responseData = "Hello app".dataUsingEncoding(NSUTF8StringEncoding)
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 | // MARK: ClientTunnelConnectionDelegate
99 |
100 | /// Handle the event of the logical flow of packets being established through the tunnel.
101 | func tunnelConnectionDidOpen(connection: ClientTunnelConnection, configuration: [NSObject: AnyObject]) {
102 |
103 | // Create the virtual interface settings.
104 | guard let settings = createTunnelSettingsFromConfiguration(configuration) else {
105 | pendingStartCompletion?(SimpleTunnelError.InternalError as NSError)
106 | pendingStartCompletion = nil
107 | return
108 | }
109 |
110 | // Set the virtual interface settings.
111 | setTunnelNetworkSettings(settings) { error in
112 | var startError: NSError?
113 | if let error = error {
114 | print("Failed to set the tunnel network settings: \(error)")
115 | startError = SimpleTunnelError.BadConfiguration as NSError
116 | }
117 | else {
118 | // Now we can start reading and writing packets to/from the virtual interface.
119 | self.tunnelConnection?.startHandlingPackets()
120 | }
121 |
122 | // Now the tunnel is fully established, call the start completion handler.
123 | self.pendingStartCompletion?(startError)
124 | self.pendingStartCompletion = nil
125 | }
126 | }
127 |
128 | /// Handle the event of the logical flow of packets being torn down.
129 | func tunnelConnectionDidClose(connection: ClientTunnelConnection, error: NSError?) {
130 | tunnelConnection = nil
131 | tunnel?.closeTunnelWithError(error)
132 | }
133 |
134 | /// Create the tunnel network settings to be applied to the virtual interface.
135 | func createTunnelSettingsFromConfiguration(configuration: [NSObject: AnyObject]) -> NEPacketTunnelNetworkSettings? {
136 | guard let tunnelAddress = tunnel?.remoteHost,
137 | address = getValueFromPlist(configuration, keyArray: [.IPv4, .Address]) as? String,
138 | netmask = getValueFromPlist(configuration, keyArray: [.IPv4, .Netmask]) as? String
139 | else { return nil }
140 |
141 | let newSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelAddress)
142 | var fullTunnel = true
143 |
144 | newSettings.IPv4Settings = NEIPv4Settings(addresses: [address], subnetMasks: [netmask])
145 |
146 | if let routes = getValueFromPlist(configuration, keyArray: [.IPv4, .Routes]) as? [[String: AnyObject]] {
147 | var includedRoutes = [NEIPv4Route]()
148 | for route in routes {
149 | if let netAddress = route[SettingsKey.Address.rawValue] as? String,
150 | netMask = route[SettingsKey.Netmask.rawValue] as? String
151 | {
152 | includedRoutes.append(NEIPv4Route(destinationAddress: netAddress, subnetMask: netMask))
153 | }
154 | }
155 | newSettings.IPv4Settings?.includedRoutes = includedRoutes
156 | fullTunnel = false
157 | }
158 | else {
159 | // No routes specified, use the default route.
160 | newSettings.IPv4Settings?.includedRoutes = [NEIPv4Route.defaultRoute()]
161 | }
162 |
163 | if let DNSDictionary = configuration[SettingsKey.DNS.rawValue] as? [String: AnyObject],
164 | DNSServers = DNSDictionary[SettingsKey.Servers.rawValue] as? [String]
165 | {
166 | newSettings.DNSSettings = NEDNSSettings(servers: DNSServers)
167 | if let DNSSearchDomains = DNSDictionary[SettingsKey.SearchDomains.rawValue] as? [String] {
168 | newSettings.DNSSettings?.searchDomains = DNSSearchDomains
169 | if !fullTunnel {
170 | newSettings.DNSSettings?.matchDomains = DNSSearchDomains
171 | }
172 | }
173 | }
174 |
175 | newSettings.tunnelOverheadBytes = 150
176 |
177 | return newSettings
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/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_erver 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 7.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) 2015 Apple Inc. All rights reserved.
53 |
--------------------------------------------------------------------------------
/SimpleTunnel/AddEditConfiguration.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 |
13 | /// A view controller object for a table view containing input fields used to specify configuration parameters for a VPN configuration.
14 | class AddEditConfiguration: ConfigurationParametersViewController {
15 |
16 | // MARK: Properties
17 |
18 | /// A table view cell containing the text field where the name of the configuration is entered.
19 | @IBOutlet weak var nameCell: TextFieldCell!
20 |
21 | /// A table view cell containing the text field where the server address of the configuration is entered.
22 | @IBOutlet weak var serverAddressCell: TextFieldCell!
23 |
24 | /// A table view cell containing the text field where the username of the configuration is entered.
25 | @IBOutlet weak var usernameCell: TextFieldCell!
26 |
27 | /// A table view cell containing the text field where the password of the configuration is entered.
28 | @IBOutlet weak var passwordCell: TextFieldCell!
29 |
30 | /// A table view cell containing a switch used to enable and disable Connect On Demand for the configuration.
31 | @IBOutlet weak var onDemandCell: SwitchCell!
32 |
33 | /// A table view cell containing a switch used to enable and disable proxy settings for the configuration.
34 | @IBOutlet weak var proxiesCell: SwitchCell!
35 |
36 | /// A table view cell containing a switch used to enable and disable Disconnect On Sleep for the configuration.
37 | @IBOutlet weak var disconnectOnSleepCell: SwitchCell!
38 |
39 | /// A table view cell that when tapped transitions the app to a view where the Connect On Demand rules are managed.
40 | @IBOutlet weak var onDemandRulesCell: UITableViewCell!
41 |
42 | /// A table view cell that when tapped transitions the app to a view where the proxy settings are managed.
43 | @IBOutlet weak var proxySettingsCell: UITableViewCell!
44 |
45 | /// The NEVPNManager object corresponding to the configuration being added or edited.
46 | var targetManager: NEVPNManager = NEVPNManager.sharedManager()
47 |
48 | // MARK: UIViewController
49 |
50 | /// Handle the event of the view being loaded into memory.
51 | override func viewDidLoad() {
52 | super.viewDidLoad()
53 |
54 | // Set up the table view cells
55 |
56 | cells = [
57 | nameCell,
58 | serverAddressCell,
59 | usernameCell,
60 | passwordCell,
61 | onDemandCell,
62 | proxiesCell,
63 | disconnectOnSleepCell
64 | ].flatMap { $0 }
65 |
66 | // The switch in proxiesCell controls the display of proxySettingsCell
67 | proxiesCell.dependentCells = [ proxySettingsCell ]
68 | proxiesCell.getIndexPath = {
69 | return self.getIndexPathOfCell(self.proxiesCell)
70 | }
71 | proxiesCell.valueChanged = {
72 | self.updateCellsWithDependentsOfCell(self.proxiesCell)
73 | }
74 |
75 | // The switch in onDemandCell controls the display of onDemandRulesCell
76 | onDemandCell.dependentCells = [ onDemandRulesCell ]
77 | onDemandCell.getIndexPath = {
78 | return self.getIndexPathOfCell(self.onDemandCell)
79 | }
80 | onDemandCell.valueChanged = {
81 | self.updateCellsWithDependentsOfCell(self.onDemandCell)
82 | self.targetManager.onDemandEnabled = self.onDemandCell.isOn
83 | }
84 |
85 | disconnectOnSleepCell.valueChanged = {
86 | self.targetManager.protocolConfiguration?.disconnectOnSleep = self.disconnectOnSleepCell.isOn
87 | }
88 |
89 | nameCell.valueChanged = {
90 | self.targetManager.localizedDescription = self.nameCell.textField.text
91 | }
92 |
93 | serverAddressCell.valueChanged = {
94 | self.targetManager.protocolConfiguration?.serverAddress = self.serverAddressCell.textField.text
95 | }
96 |
97 | usernameCell.valueChanged = {
98 | self.targetManager.protocolConfiguration?.username = self.usernameCell.textField.text
99 | }
100 | }
101 |
102 | /// Handle the event of the view being displayed.
103 | override func viewWillAppear(animated: Bool) {
104 | super.viewWillAppear(animated)
105 |
106 | tableView.reloadData()
107 |
108 | // Set the text fields and switches per the settings in the configuration.
109 |
110 | nameCell.textField.text = targetManager.localizedDescription
111 | serverAddressCell.textField.text = targetManager.protocolConfiguration?.serverAddress
112 | usernameCell.textField.text = targetManager.protocolConfiguration?.username
113 |
114 | if let passRef = targetManager.protocolConfiguration?.passwordReference {
115 | passwordCell.textField.text = getPasswordWithPersistentReference(passRef)
116 | }
117 | else {
118 | passwordCell.textField.text = nil
119 | }
120 |
121 | disconnectOnSleepCell.isOn = targetManager.protocolConfiguration?.disconnectOnSleep ?? false
122 |
123 | onDemandCell.isOn = targetManager.onDemandEnabled
124 |
125 | onDemandRulesCell.detailTextLabel?.text = getDescriptionForListValue(targetManager.onDemandRules, itemDescription: "rule")
126 |
127 | proxiesCell.isOn = targetManager.protocolConfiguration?.proxySettings != nil
128 | }
129 |
130 | /// Set up the destination view controller of a segue away from this view controller.
131 | override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
132 | guard let identifier = segue.identifier else { return }
133 |
134 | switch identifier {
135 | case "edit-proxy-settings":
136 | // The user tapped on the proxy settings cell.
137 | guard let controller = segue.destinationViewController as? ProxySettingsController else { break }
138 | if targetManager.protocolConfiguration?.proxySettings == nil {
139 | targetManager.protocolConfiguration?.proxySettings = NEProxySettings()
140 | }
141 | controller.targetConfiguration = targetManager.protocolConfiguration ?? NETunnelProviderProtocol()
142 |
143 | case "edit-on-demand-rules":
144 | // The user tapped on the Connect On Demand rules cell.
145 | guard let controller = segue.destinationViewController as? OnDemandRuleListController else { break }
146 | controller.targetManager = targetManager
147 |
148 | default:
149 | break
150 | }
151 | }
152 |
153 | // MARK: Interface
154 |
155 | /// Set the target configuration and the title to display in the view.
156 | func setTargetManager(manager: NEVPNManager?, title: String?) {
157 | if let newManager = manager {
158 | // A manager was given, so an existing configuration is being edited.
159 | targetManager = newManager
160 | }
161 | else {
162 | // No manager was given, create a new configuration.
163 | let newManager = NETunnelProviderManager()
164 | newManager.protocolConfiguration = NETunnelProviderProtocol()
165 | newManager.localizedDescription = "Demo VPN"
166 | newManager.protocolConfiguration?.serverAddress = "TunnelServer"
167 | targetManager = newManager
168 | }
169 | navigationItem.title = title
170 | }
171 |
172 | /// Save the configuration to the Network Extension preferences.
173 | @IBAction func saveTargetManager(sender: AnyObject) {
174 | if !proxiesCell.isOn {
175 | targetManager.protocolConfiguration?.proxySettings = nil
176 | }
177 | targetManager.saveToPreferencesWithCompletionHandler { error in
178 | if let saveError = error {
179 | print("Failed to save the configuration: \(saveError)")
180 | return
181 | }
182 |
183 | // Transition back to the configuration list view.
184 | self.performSegueWithIdentifier("save-configuration", sender: sender)
185 | }
186 | }
187 |
188 | /// Save a password in the keychain.
189 | func savePassword(password: String, inKeychainItem: NSData?) -> NSData? {
190 | guard let passwordData = password.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false) else { return nil }
191 | var status = errSecSuccess
192 |
193 | if let persistentReference = inKeychainItem {
194 | // A persistent reference was given, update the corresponding keychain item.
195 | let query: [NSObject: AnyObject] = [
196 | kSecValuePersistentRef : persistentReference,
197 | kSecReturnAttributes : kCFBooleanTrue
198 | ]
199 | var result: AnyObject?
200 |
201 | // Get the current attributes for the item.
202 | status = SecItemCopyMatching(query, &result)
203 |
204 | if let attributes = result as? [NSObject: AnyObject] where status == errSecSuccess {
205 | // Update the attributes with the new data.
206 | var updateQuery = [NSObject: AnyObject]()
207 | updateQuery[kSecClass] = kSecClassGenericPassword
208 | updateQuery[kSecAttrService] = attributes[kSecAttrService]
209 |
210 | var newAttributes = attributes
211 | newAttributes[kSecValueData] = passwordData
212 |
213 | status = SecItemUpdate(updateQuery, newAttributes)
214 | if status == errSecSuccess {
215 | return persistentReference
216 | }
217 | }
218 | }
219 |
220 | if inKeychainItem == nil || status != errSecSuccess {
221 | // No persistent reference was provided, or the update failed. Add a new keychain item.
222 |
223 | let attributes: [NSObject: AnyObject] = [
224 | kSecAttrService : NSUUID().UUIDString,
225 | kSecValueData : passwordData,
226 | kSecAttrAccessible : kSecAttrAccessibleAlways,
227 | kSecClass : kSecClassGenericPassword,
228 | kSecReturnPersistentRef : kCFBooleanTrue
229 | ]
230 |
231 | var result: AnyObject?
232 | status = SecItemAdd(attributes, &result)
233 |
234 | if let newPersistentReference = result as? NSData where status == errSecSuccess {
235 | return newPersistentReference
236 | }
237 | }
238 | return nil
239 | }
240 |
241 | /// Remove a password from the keychain.
242 | func removePasswordWithPersistentReference(persistentReference: NSData) {
243 | let query: [NSObject: AnyObject] = [
244 | kSecClass : kSecClassGenericPassword,
245 | kSecValuePersistentRef : persistentReference
246 | ]
247 |
248 | let status = SecItemDelete(query)
249 | if status != errSecSuccess {
250 | print("Failed to delete a password: \(status)")
251 | }
252 | }
253 |
254 | /// Get a password from the keychain.
255 | func getPasswordWithPersistentReference(persistentReference: NSData) -> String? {
256 | var result: String?
257 | let query: [NSObject: AnyObject] = [
258 | kSecClass : kSecClassGenericPassword,
259 | kSecReturnData : kCFBooleanTrue,
260 | kSecValuePersistentRef : persistentReference
261 | ]
262 |
263 | var returnValue: AnyObject?
264 | let status = SecItemCopyMatching(query, &returnValue)
265 |
266 | if let passwordData = returnValue as? NSData where status == errSecSuccess {
267 | result = NSString(data: passwordData, encoding: NSUTF8StringEncoding) as? String
268 | }
269 | return result
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/SimpleTunnel/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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) 2015 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
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: [NSData], 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,
66 | TunnelMessageKey.Protocols.rawValue: protocols
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.readPacketsWithCompletionHandler { inPackets, inProtocols in
77 | self.handlePackets(inPackets, protocols: inProtocols)
78 | }
79 | }
80 | }
81 |
82 | /// Make the initial readPacketsWithCompletionHandler call.
83 | func startHandlingPackets() {
84 | packetFlow.readPacketsWithCompletionHandler { 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? [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: [NSData], protocols: [NSNumber]) {
109 | packetFlow.writePackets(packets, withProtocols: protocols)
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/SimpleTunnel/ConfigurationListController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 |
12 | /// The view controller object for the list of packet tunnel configurations.
13 | class ConfigurationListController: ListViewController {
14 |
15 | // MARK: Properties
16 |
17 | /// The image to display for configurations that are disabled.
18 | let disabledImage = UIImage(named: "GrayDot")
19 |
20 | /// The image to display for configurations that are enabled but are disconnected.
21 | let disconnectedImage = UIImage(named: "RedDot")
22 |
23 | /// The image to display for configurations that are active (or not disconnected).
24 | let notDisconnectedImage = UIImage(named: "GreenDot")
25 |
26 | /// A list of NEVPNManager objects for the packet tunnel configurations.
27 | var managers = [NEVPNManager]()
28 |
29 | // MARK: UIViewController
30 |
31 | /// Handle the event of the view loading into memory.
32 | override func viewDidLoad() {
33 | isAddEnabled = true
34 | super.viewDidLoad()
35 | }
36 |
37 | /// Handle the event of the view being displayed.
38 | override func viewWillAppear(animated: Bool) {
39 | super.viewWillAppear(animated)
40 |
41 | // Re-load all of the configurations.
42 | reloadManagers()
43 | }
44 |
45 | // MARK: Interface
46 |
47 | /// Re-load all of the packet tunnel configurations from the Network Extension preferences
48 | func reloadManagers() {
49 | NETunnelProviderManager.loadAllFromPreferencesWithCompletionHandler() { newManagers, error in
50 | guard let vpnManagers = newManagers else { return }
51 |
52 | self.stopObservingStatus()
53 | self.managers = vpnManagers
54 | self.observeStatus()
55 |
56 | // If there are no configurations, automatically go into editing mode.
57 | if self.managers.count == 0 && !self.editing {
58 | self.setEditing(true, animated: false)
59 | }
60 |
61 | self.tableView.reloadData()
62 | }
63 | }
64 |
65 | /// Register for configuration change notifications.
66 | func observeStatus() {
67 | for (index, manager) in managers.enumerate() {
68 | NSNotificationCenter.defaultCenter().addObserverForName(NEVPNStatusDidChangeNotification, object: manager.connection, queue: NSOperationQueue.mainQueue(), usingBlock: { notification in
69 | self.tableView.reloadRowsAtIndexPaths([ NSIndexPath(forRow: index, inSection: 0) ], withRowAnimation: .Fade)
70 | })
71 | }
72 | }
73 |
74 | /// De-register for configuration change notifications.
75 | func stopObservingStatus() {
76 | for manager in managers {
77 | NSNotificationCenter.defaultCenter().removeObserver(self, name: NEVPNStatusDidChangeNotification, object: manager.connection)
78 | }
79 | }
80 |
81 | /// Unwind segue handler.
82 | @IBAction func handleUnwind(sender: UIStoryboardSegue) {
83 | }
84 |
85 | // MARK: ListViewController
86 |
87 | /// Set up the destination view controller of a segue away from this view controller.
88 | override func listSetupSegue(segue: UIStoryboardSegue, forItemAtIndex index: Int) {
89 |
90 | guard let identifier = segue.identifier else { return }
91 |
92 | switch identifier {
93 | case "start-new-configuration":
94 | // The user tapped on the "Add Configuration..." table row.
95 | guard let addEditController = segue.destinationViewController as? AddEditConfiguration else { break }
96 |
97 | addEditController.setTargetManager(nil, title: "Add Configuration")
98 |
99 | case "edit-configuration":
100 | // The user tapped on the disclosure button of a configuration while in editing mode.
101 | guard let addEditController = segue.destinationViewController as? AddEditConfiguration where index < managers.count else { break }
102 |
103 | addEditController.setTargetManager(managers[index], title: managers[index].localizedDescription)
104 |
105 | case "introspect-status":
106 | // The user tapped on a configuration while not in editing mode.
107 | guard let statusViewController = segue.destinationViewController as? StatusViewController where index < managers.count else { break }
108 |
109 | statusViewController.targetManager = managers[index]
110 |
111 | default:
112 | break
113 | }
114 | }
115 |
116 | /// The current number of configurations.
117 | override var listCount: Int {
118 | return managers.count
119 | }
120 |
121 | /// Returns the localized description of the configuration at the given index.
122 | override func listTextForItemAtIndex(index: Int) -> String {
123 | return managers[index].localizedDescription ?? "NoName"
124 | }
125 |
126 | /// Returns the appropriate image for the configuration at the given index.
127 | override func listImageForItemAtIndex(index: Int) -> UIImage? {
128 | let manager = managers[index]
129 |
130 | guard manager.enabled else { return disabledImage }
131 |
132 | switch manager.connection.status {
133 | case .Invalid: fallthrough
134 | case .Disconnected: return disconnectedImage
135 | default: return notDisconnectedImage
136 | }
137 | }
138 |
139 | /// Returns UITableViewCellAccessoryType.DisclosureIndicator.
140 | override var listAccessoryType: UITableViewCellAccessoryType {
141 | return .DisclosureIndicator
142 | }
143 |
144 | /// Returns UITableViewCellAccessoryType.DetailButton.
145 | override var listEditingAccessoryType: UITableViewCellAccessoryType {
146 | return .DetailButton
147 | }
148 |
149 | /// Handle a user tap on the "Delete" button for a configuration.
150 | override func listRemoveItemAtIndex(index: Int) {
151 |
152 | // Remove the configuration from the Network Extension preferences.
153 | managers[index].removeFromPreferencesWithCompletionHandler {
154 | error in
155 | if let error = error {
156 | print("Failed to remove manager: \(error)")
157 | }
158 | }
159 | managers.removeAtIndex(index)
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/SimpleTunnel/ConfigurationParametersViewController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 numberOfSectionsInTableView(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, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
33 | return cells[indexPath.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 | where !cell.dependentCells.isEmpty
42 | {
43 | let index = indexPath.row + 1
44 | if cell.isOn {
45 | cells.splice(cell.dependentCells, atIndex: index)
46 | }
47 | else {
48 | let removeRange = Range(start: index, end: index + cell.dependentCells.count)
49 | cells.removeRange(removeRange)
50 | }
51 | }
52 | }
53 |
54 | /// Return the index of a given cell in the cells list.
55 | func getIndexPathOfCell(cell: UITableViewCell) -> NSIndexPath? {
56 | if let row = cells.indexOf({ $0 == cell }) {
57 | return NSIndexPath(forRow: row, inSection: 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 where !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 where !list.isEmpty {
74 | return (list.count <= 3 ? ", ".join(list) : "\(list.count) \(itemDescription)s")
75 | } else {
76 | return placeHolder
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/SimpleTunnel/ConnectionRuleAddEditController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 = NSURL(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 prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
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.destinationViewController 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.destinationViewController as? EnumPickerController else { break }
101 |
102 | let enumValues: [NEEvaluateConnectionRuleAction] = [ .ConnectIfNeeded, .NeverConnect, ],
103 | stringValues = enumValues.flatMap { $0.description },
104 | currentSelection = enumValues.indexOf { $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.destinationViewController 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: (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 | performSegueWithIdentifier("save-connection-rule", sender: sender)
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/SimpleTunnel/ConnectionRuleListController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 | ruleAddEditController = segue.destinationViewController 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?.removeAtIndex(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) 2015 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.sharedManager().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.sharedManager().providerConfiguration = newConfiguration
52 | }
53 | NEFilterManager.sharedManager().enabled = self.statusCell.isOn
54 | NEFilterManager.sharedManager().saveToPreferencesWithCompletionHandler { error in
55 | if let saveError = error {
56 | print("Failed to save the filter configuration: \(saveError)")
57 | self.statusCell.isOn = false
58 | return
59 | }
60 | self.rulesServerCell.textField.text = NEFilterManager.sharedManager().providerConfiguration?.serverAddress
61 | FilterUtilities.defaults?.setValue(NEFilterManager.sharedManager().providerConfiguration?.serverAddress, forKey: "serverAddress")
62 | }
63 | }
64 |
65 | rulesServerCell.valueChanged = {
66 | guard let serverIPAddress = self.rulesServerCell.textField.text where !serverIPAddress.isEmpty else { return }
67 |
68 | NEFilterManager.sharedManager().providerConfiguration?.serverAddress = serverIPAddress
69 | NEFilterManager.sharedManager().saveToPreferencesWithCompletionHandler { error in
70 | if let saveError = error {
71 | print("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.sharedManager().loadFromPreferencesWithCompletionHandler { error in
84 | if let loadError = error {
85 | print("Failed to load the filter configuration: \(loadError)")
86 | self.statusCell.isOn = false
87 | return
88 | }
89 |
90 | self.statusCell.isOn = NEFilterManager.sharedManager().enabled
91 | self.rulesServerCell.textField.text = NEFilterManager.sharedManager().providerConfiguration?.serverAddress
92 |
93 | self.reloadRules()
94 | }
95 | }
96 |
97 | // MARK: NSObject
98 |
99 | /// Handle changes to the rules
100 | override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) {
101 | if context == &rulesContext && keyPath == "rules" {
102 | reloadRules()
103 | } else {
104 | super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
105 | }
106 | }
107 |
108 | // MARK: UITableViewDataSource
109 |
110 | /// Return the number of sections to display.
111 | override func numberOfSectionsInTableView(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, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
126 | if indexPath.section == 0 {
127 | return indexPath.row == 0 ? statusCell : rulesServerCell
128 | }
129 | else if let cell = tableView.dequeueReusableCellWithIdentifier("rule-cell") {
130 | let (hostString, actionString) = currentRules[indexPath.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?.objectForKey("rules") as? [String : [String : AnyObject]] else { return }
152 |
153 | for (hostname, ruleInfo) in rules {
154 | guard let ruleActionNum = ruleInfo["kRule"] as? Int,
155 | 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.sharedManager().providerConfiguration?.serverAddress)
166 | reloadRules()
167 | }
168 |
169 | }
170 |
--------------------------------------------------------------------------------
/SimpleTunnel/EnumPickerController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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: (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 numberOfComponentsInPickerView(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 ?? 0
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/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@1x.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/GrayDot.imageset/GrayDot@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/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/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@1x.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/GreenDot.imageset/GreenDot@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/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/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@1x.png
--------------------------------------------------------------------------------
/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ios-sample-code/SimpleTunnel/6b4f18b409af7adc03d094e63ff9a9e60ee9bf83/SimpleTunnel/Images.xcassets/RedDot.imageset/RedDot@3x.png
--------------------------------------------------------------------------------
/SimpleTunnel/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIVPNPlugin
6 |
7 | ${PLUGIN_BUNDLE_ID}
8 |
9 | CFBundleDevelopmentRegion
10 | en
11 | CFBundleExecutable
12 | $(EXECUTABLE_NAME)
13 | CFBundleIdentifier
14 | $(PRODUCT_BUNDLE_IDENTIFIER)
15 | CFBundleInfoDictionaryVersion
16 | 6.0
17 | CFBundleName
18 | $(PRODUCT_NAME)
19 | CFBundlePackageType
20 | APPL
21 | CFBundleShortVersionString
22 | 1.0
23 | CFBundleSignature
24 | ????
25 | CFBundleVersion
26 | 1
27 | LSRequiresIPhoneOS
28 |
29 | NETestAppMapping_disabled
30 |
31 | com.apple.vpntest.jpw.plugin-test.applayer
32 |
33 | com.apple.vpntest.BasicBrowser
34 |
35 | com.apple.vpntest.jpw-test-per-app-vpn-secondary
36 |
37 | com.google.chrome.ios
38 | com.linkedin.app
39 |
40 |
41 | UILaunchStoryboardName
42 | LaunchScreen
43 | UIMainStoryboardFile
44 | Main
45 | UIRequiredDeviceCapabilities
46 |
47 | armv7
48 |
49 | UISupportedInterfaceOrientations
50 |
51 | UIInterfaceOrientationPortrait
52 | UIInterfaceOrientationLandscapeLeft
53 | UIInterfaceOrientationLandscapeRight
54 | UIInterfaceOrientationPortraitUpsideDown
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationPortraitUpsideDown
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/SimpleTunnel/ListViewController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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.editing = 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 prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
79 | guard let cell = sender as? UITableViewCell,
80 | indexPath = tableView.indexPathForCell(cell)
81 | else { return }
82 |
83 | listSetupSegue(segue, forItemAtIndex: indexPath.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 = NSIndexPath(forItem: listCount, inSection: 0)
95 | if editing {
96 | tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Bottom)
97 | }
98 | else {
99 | tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Bottom)
100 | }
101 | }
102 | }
103 |
104 | // MARK: UITableViewDataSource
105 |
106 | /// Always returns 1.
107 | override func numberOfSectionsInTableView(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.editing {
115 | count++
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, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
122 | var cell = UITableViewCell()
123 | if indexPath.item < listCount {
124 | // Return a cell containing details about an item in the list.
125 | if let itemCell = tableView.dequeueReusableCellWithIdentifier("item-cell") {
126 | cell = itemCell
127 | cell.textLabel?.text = listTextForItemAtIndex(indexPath.row)
128 | cell.accessoryType = listAccessoryType
129 | cell.editingAccessoryType = listEditingAccessoryType
130 | cell.imageView?.image = listImageForItemAtIndex(indexPath.row)
131 | }
132 | }
133 | else if tableView.editing && indexPath.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.dequeueReusableCellWithIdentifier("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, canEditRowAtIndexPath indexPath: NSIndexPath) -> 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, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
155 | switch editingStyle {
156 | case .Delete:
157 | listRemoveItemAtIndex(indexPath.row)
158 | tableView.deleteRowsAtIndexPaths([ indexPath ], withRowAnimation: .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, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle {
168 | if indexPath.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.insertRowsAtIndexPaths([ NSIndexPath(forRow: index, inSection: 0) ], withRowAnimation: .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 | }
--------------------------------------------------------------------------------
/SimpleTunnel/OnDemandRuleAddEditController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 = NSURL(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 prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
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.destinationViewController 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.destinationViewController as? EnumPickerController else { break }
140 |
141 | let enumValues: [NEOnDemandRuleInterfaceType] = [ .Any, .WiFi, .Cellular ],
142 | stringValues = enumValues.flatMap { $0.description },
143 | currentSelection = enumValues.indexOf { $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.destinationViewController 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.destinationViewController 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.destinationViewController as? EnumPickerController else { break }
168 |
169 | let enumValues: [NEOnDemandRuleAction] = [ .EvaluateConnection, .Disconnect, .Connect, .Ignore ],
170 | stringValues = enumValues.flatMap { $0.description },
171 | currentSelection = enumValues.indexOf { $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.destinationViewController as? ConnectionRuleListController,
180 | 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: (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.removeAtIndex(rulesIndexPath.row)
244 | self.tableView.deleteRowsAtIndexPaths([ rulesIndexPath ], withRowAnimation: .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, atIndex: actionIndexPath.row + 1)
250 | let indexPaths = [ NSIndexPath(forRow: actionIndexPath.row + 1, inSection: actionIndexPath.section) ]
251 | self.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .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.performSegueWithIdentifier("save-on-demand-rule", sender: sender)
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/SimpleTunnel/OnDemandRuleListController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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.sharedManager()
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 | ruleAddEditController = segue.destinationViewController 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?.removeAtIndex(index)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/SimpleTunnel/ProxyAutoConfigScriptController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 | performSegueWithIdentifier("save-proxy-script", sender: sender)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SimpleTunnel/ProxyServerAddEditController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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: 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 | portString = portCell.textField.text,
86 | port = Int(portString)
87 | where !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 | performSegueWithIdentifier("save-proxy-server-settings", sender: sender)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/SimpleTunnel/ProxySettingsController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 = NSURL(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 = 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 prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
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.destinationViewController 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.destinationViewController 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.destinationViewController as? ProxyServerAddEditController else { break }
167 |
168 | proxyServerController.setTargetServer(targetConfiguration.proxySettings?.HTTPSServer, title: "HTTPS Proxy Server") { newServer in
169 | 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.destinationViewController as? ProxyServerAddEditController else { break }
175 |
176 | proxyServerController.setTargetServer(targetConfiguration.proxySettings?.HTTPServer, title: "HTTP Proxy Server") { newServer in
177 | 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.destinationViewController as? ProxyAutoConfigScriptController else { break }
183 |
184 | pacScriptController.scriptText?.text = targetConfiguration.proxySettings?.proxyAutoConfigurationJavaScript
185 | pacScriptController.saveScriptCallback = { newScript in
186 | if let script = newScript where !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 | 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 |
--------------------------------------------------------------------------------
/SimpleTunnel/StatusViewController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 |
12 | // MARK: Extensions
13 |
14 | /// Make NEVPNStatus convertible to a string
15 | extension NEVPNStatus: CustomStringConvertible {
16 | public var description: String {
17 | switch self {
18 | case .Disconnected: return "Disconnected"
19 | case .Invalid: return "Invalid"
20 | case .Connected: return "Connected"
21 | case .Connecting: return "Connecting"
22 | case .Disconnecting: return "Disconnecting"
23 | case .Reasserting: return "Reconnecting"
24 | }
25 | }
26 | }
27 |
28 | /// A view controller object for a view that displays VPN status information and allows the user to start and stop the VPN.
29 | class StatusViewController: UITableViewController {
30 |
31 | // MARK: Properties
32 |
33 | /// A switch that toggles the enabled state of the VPN configuration.
34 | @IBOutlet weak var enabledSwitch: UISwitch!
35 |
36 | /// A switch that starts and stops the VPN.
37 | @IBOutlet weak var startStopToggle: UISwitch!
38 |
39 | /// A label that contains the current status of the VPN.
40 | @IBOutlet weak var statusLabel: UILabel!
41 |
42 | /// The target VPN configuration.
43 | var targetManager = NEVPNManager.sharedManager()
44 |
45 | // MARK: UIViewController
46 |
47 | /// Handle the event where the view is being displayed.
48 | override func viewWillAppear(animated: Bool) {
49 | super.viewWillAppear(animated)
50 |
51 | // Initialize the UI
52 | enabledSwitch.on = targetManager.enabled
53 | startStopToggle.on = (targetManager.connection.status != .Disconnected && targetManager.connection.status != .Invalid)
54 | statusLabel.text = targetManager.connection.status.description
55 | navigationItem.title = targetManager.localizedDescription
56 |
57 | // Register to be notified of changes in the status.
58 | NSNotificationCenter.defaultCenter().addObserverForName(NEVPNStatusDidChangeNotification, object: targetManager.connection, queue: NSOperationQueue.mainQueue(), usingBlock: { notification in
59 | self.statusLabel.text = self.targetManager.connection.status.description
60 | self.startStopToggle.on = (self.targetManager.connection.status != .Disconnected && self.targetManager.connection.status != .Disconnecting && self.targetManager.connection.status != .Invalid)
61 | })
62 |
63 | // Disable the start/stop toggle if the configuration is not enabled.
64 | startStopToggle.enabled = enabledSwitch.on
65 |
66 | // Send a simple IPC message to the provider, handle the response.
67 | if let session = targetManager.connection as? NETunnelProviderSession,
68 | message = "Hello Provider".dataUsingEncoding(NSUTF8StringEncoding)
69 | where targetManager.connection.status != .Invalid
70 | {
71 | do {
72 | try session.sendProviderMessage(message) { response in
73 | if response != nil {
74 | let responseString = NSString(data: response!, encoding: NSUTF8StringEncoding)
75 | print("Received response from the provider: \(responseString)")
76 | } else {
77 | print("Got a nil response from the provider")
78 | }
79 | }
80 | } catch {
81 | print("Failed to send a message to the provider")
82 | }
83 | }
84 | }
85 |
86 | /// Handle the event where the view is being hidden.
87 | override func viewWillDisappear(animated: Bool) {
88 | super.viewWillDisappear(animated)
89 |
90 | // Stop watching for status change notifications.
91 | NSNotificationCenter.defaultCenter().removeObserver(self, name: NEVPNStatusDidChangeNotification, object: targetManager.connection)
92 | }
93 |
94 | /// Handle the user toggling the "enabled" switch.
95 | @IBAction func enabledToggled(sender: AnyObject) {
96 | targetManager.enabled = enabledSwitch.on
97 | targetManager.saveToPreferencesWithCompletionHandler { error in
98 | guard error == nil else {
99 | self.enabledSwitch.on = self.targetManager.enabled
100 | self.startStopToggle.enabled = self.enabledSwitch.on
101 | return
102 | }
103 |
104 | self.targetManager.loadFromPreferencesWithCompletionHandler { error in
105 | self.enabledSwitch.on = self.targetManager.enabled
106 | self.startStopToggle.enabled = self.enabledSwitch.on
107 | }
108 | }
109 | }
110 |
111 | /// Handle the user toggling the "VPN" switch.
112 | @IBAction func startStopToggled(sender: AnyObject) {
113 | if targetManager.connection.status == .Disconnected || targetManager.connection.status == .Invalid {
114 | do {
115 | try targetManager.connection.startVPNTunnel()
116 | }
117 | catch {
118 | print("Failed to start the VPN: \(error)")
119 | }
120 | }
121 | else {
122 | targetManager.connection.stopVPNTunnel()
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/SimpleTunnel/StringListController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 ?? 0
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.removeAtIndex(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: ([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) 2015 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 -> NSIndexPath?)?
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.on
35 | }
36 | set (newValue) {
37 | let changed = toggle.on != newValue
38 | toggle.on = 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 = [NSIndexPath]()
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(NSIndexPath(forRow: switchIndexPath.row + row, inSection: switchIndexPath.section))
64 | }
65 |
66 | guard !indexPaths.isEmpty else { return }
67 |
68 | if toggle.on {
69 | tableViewController.tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Bottom)
70 | } else {
71 | tableViewController.tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: .Bottom)
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/SimpleTunnel/TextFieldCell.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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) 2015 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 | /// The client-side implementation of the SimpleTunnel protocol.
13 | public class ClientTunnel: Tunnel {
14 |
15 | // MARK: Properties
16 |
17 | /// The tunnel connection.
18 | public var connection: NWTCPConnection?
19 |
20 | /// The last error that occurred on the tunnel.
21 | public var lastError: NSError?
22 |
23 | /// The previously-received incomplete message data.
24 | var previousData: NSMutableData?
25 |
26 | /// The address of the tunnel server.
27 | public var remoteHost: String?
28 |
29 | // MARK: Interface
30 |
31 | /// Start the TCP connection to the tunnel server.
32 | public func startTunnel(provider: NETunnelProvider) -> SimpleTunnelError? {
33 |
34 | guard let serverAddress = provider.protocolConfiguration.serverAddress else {
35 | return .BadConfiguration
36 | }
37 |
38 | let endpoint: NWEndpoint
39 |
40 | if let colonRange = serverAddress.rangeOfCharacterFromSet(NSCharacterSet(charactersInString: ":"), options: [], range: nil) {
41 | // The server is specified in the configuration as :.
42 | let hostname = serverAddress.substringWithRange(Range(start:serverAddress.startIndex, end:colonRange.startIndex))
43 | let portString = serverAddress.substringWithRange(Range(start:colonRange.startIndex.successor(), end:serverAddress.endIndex))
44 |
45 | guard !hostname.isEmpty && !portString.isEmpty else {
46 | return .BadConfiguration
47 | }
48 |
49 | endpoint = NWHostEndpoint(hostname:hostname, port:portString)
50 | }
51 | else {
52 | // The server is specified in the configuration as a Bonjour service name.
53 | endpoint = NWBonjourServiceEndpoint(name: serverAddress, type:Tunnel.serviceType, domain:Tunnel.serviceDomain)
54 | }
55 |
56 | // Kick off the connection to the server.
57 | connection = provider.createTCPConnectionToEndpoint(endpoint, enableTLS:false, TLSParameters:nil, delegate:nil)
58 |
59 | // Register for notificationes when the connection status changes.
60 | connection!.addObserver(self, forKeyPath: "state", options: .Initial, context: &connection)
61 |
62 | return nil
63 | }
64 |
65 | /// Close the tunnel.
66 | public func closeTunnelWithError(error: NSError?) {
67 | lastError = error
68 | closeTunnel()
69 | }
70 |
71 | /// Handle data read from the tunnel connection.
72 | func handleReadEvent(data: NSData?, error: NSError?) {
73 | if let readError = error {
74 | print("Got an error on the tunnel connection: \(readError)")
75 | closeTunnelWithError(readError)
76 | return
77 | }
78 | guard let newData = data else {
79 | // EOF
80 | closeTunnel()
81 | return
82 | }
83 |
84 | // If there is a previously-read incomplete message, append the new data to what was previously read.
85 | var currentData = newData
86 | if let oldData = previousData {
87 | oldData.appendData(newData)
88 | currentData = oldData
89 | previousData = nil
90 | }
91 |
92 | // Start out by looking at all of the data.
93 | var currentRange = Range(start: 0, end: currentData.length)
94 |
95 | while currentRange.count > sizeof(UInt32.self) {
96 | var totalLength: UInt32 = 0
97 |
98 | // Parse out the total length of the message, which is stored in the first 4 bytes of the message.
99 | let lengthRange = Range(start: currentRange.startIndex, end: currentRange.startIndex + sizeofValue(totalLength))
100 | currentData.getBytes(&totalLength, range: NSRange(lengthRange))
101 |
102 | // If we don't have the entire message, stop parsing.
103 | guard currentRange.count >= Int(totalLength) else { break }
104 |
105 | // Move past the first 4 bytes holding the total length.
106 | currentRange = rangeByMovingStartOfRange(currentRange, byCount: sizeofValue(totalLength))
107 |
108 | // Subtract the size of the total length from the total length of the message to get the message payload length.
109 | let payloadLength = Int(totalLength - UInt32(sizeofValue(totalLength)))
110 |
111 | // Get the payload and handle the message.
112 | let range = Range(start: currentRange.startIndex, end: currentRange.startIndex + payloadLength)
113 | handlePacket(currentData.subdataWithRange(NSRange(range)))
114 |
115 | // Move past the payload.
116 | currentRange = rangeByMovingStartOfRange(currentRange, byCount: payloadLength)
117 | }
118 |
119 | // If we have data left, then save the incomplete message for when we get more data from the tunnel connection.
120 | if !currentRange.isEmpty {
121 | previousData = NSMutableData(data: currentData.subdataWithRange(NSRange(currentRange)))
122 | }
123 |
124 | guard let targetConnection = connection else { return }
125 |
126 | // Kick off another read operation.
127 | targetConnection.readMinimumLength(sizeof(UInt32.self), maximumLength: Tunnel.packetSize) { data, error in
128 | self.handleReadEvent(data, error: error)
129 | }
130 | }
131 |
132 | /// Send a message to the tunnel server.
133 | public func sendMessage(messageProperties: [String: AnyObject], completionHandler: (NSError?) -> Void) {
134 | guard let messageData = serializeMessage(messageProperties) else {
135 | completionHandler(SimpleTunnelError.InternalError as NSError)
136 | return
137 | }
138 |
139 | connection?.write(messageData, completionHandler: completionHandler)
140 | }
141 |
142 | // MARK: NSObject
143 |
144 | /// Handle changes to the tunnel connection state.
145 | public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String: AnyObject]?, context: UnsafeMutablePointer) {
146 | guard keyPath == "state" && UnsafeMutablePointer(context).memory == connection else {
147 | super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
148 | return
149 | }
150 |
151 | switch connection!.state {
152 | case .Connected:
153 | // Let the delegate know that the tunnel is open, and start reading from the tunnel connection.
154 | delegate?.tunnelDidOpen(self)
155 | connection!.readMinimumLength(sizeof(UInt32.self), maximumLength: Tunnel.packetSize) { data, error in
156 | self.handleReadEvent(data, error: error)
157 | }
158 |
159 | case .Disconnected:
160 | closeTunnelWithError(connection!.error)
161 |
162 | case .Cancelled:
163 | connection!.removeObserver(self, forKeyPath:"state", context:&connection)
164 | connection = nil
165 | delegate?.tunnelDidClose(self)
166 |
167 | default:
168 | break
169 | }
170 | }
171 |
172 | // MARK: Tunnel
173 |
174 | /// Close the tunnel.
175 | override public func closeTunnel() {
176 | // Close the tunnel connection.
177 | if let TCPConnection = connection {
178 | TCPConnection.cancel()
179 | }
180 | super.closeTunnel()
181 | }
182 |
183 | /// Write data to the tunnel connection.
184 | override func writeDataToTunnel(data: NSData, startingAtOffset: Int) -> Int {
185 | connection?.write(data) { error in
186 | self.closeTunnelWithError(error)
187 | }
188 | return data.length
189 | }
190 |
191 | /// Handle a message received from the tunnel server.
192 | override func handleMessage(commandType: TunnelCommand, properties: [String: AnyObject], connection: Connection?) -> Bool {
193 | var success = true
194 |
195 | switch commandType {
196 | case .OpenResult:
197 | // A logical connection was opened successfully.
198 | guard let targetConnection = connection,
199 | resultCodeNumber = properties[TunnelMessageKey.ResultCode.rawValue] as? Int,
200 | resultCode = TunnelConnectionOpenResult(rawValue: resultCodeNumber)
201 | else
202 | {
203 | success = false
204 | break
205 | }
206 |
207 | if let remoteAddress = self.connection!.remoteAddress as? NWHostEndpoint {
208 | remoteHost = remoteAddress.hostname
209 | }
210 | targetConnection.handleOpenCompleted(resultCode, properties:properties)
211 |
212 | default:
213 | print("Tunnel received an invalid command")
214 | success = false
215 | }
216 | return success
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/SimpleTunnelServices/Connection.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 {
14 | case None = 1
15 | case Read = 2
16 | case Write = 3
17 | case All = 4
18 | }
19 |
20 | /// The results of opening a connection.
21 | public enum TunnelConnectionOpenResult: Int {
22 | case Success = 0
23 | case InvalidParam
24 | case NoSuchHost
25 | case Refused
26 | case Timeout
27 | case InternalError
28 | }
29 |
30 | /// A logical connection (or flow) of network data in the SimpleTunnel protocol.
31 | public class Connection: NSObject {
32 |
33 | // MARK: Properties
34 |
35 | /// The connection identifier.
36 | public let identifier: Int
37 |
38 | /// The tunnel that contains the connection.
39 | public var tunnel: Tunnel?
40 |
41 | /// The list of data that needs to be written to the connection when possible.
42 | let savedData = SavedData()
43 |
44 | /// The direction(s) in which the connection is closed.
45 | var currentCloseDirection = TunnelConnectionCloseDirection.None
46 |
47 | /// Indicates if the tunnel is being used by this connection exclusively.
48 | let isExclusiveTunnel: Bool
49 |
50 | /// Indicates if the connection cannot be read from.
51 | public var isClosedForRead: Bool {
52 | return currentCloseDirection != .None && currentCloseDirection != .Write
53 | }
54 |
55 | /// Indicates if the connection cannot be written to.
56 | public var isClosedForWrite: Bool {
57 | return currentCloseDirection != .None && currentCloseDirection != .Read
58 | }
59 |
60 | /// Indicates if the connection is fully closed.
61 | public var isClosedCompletely: Bool {
62 | return currentCloseDirection == .All
63 | }
64 |
65 | // MARK: Initializers
66 |
67 | public init(connectionIdentifier: Int, parentTunnel: Tunnel) {
68 | tunnel = parentTunnel
69 | identifier = connectionIdentifier
70 | isExclusiveTunnel = false
71 | super.init()
72 | if let t = tunnel {
73 | // Add this connection to the tunnel's set of connections.
74 | t.addConnection(self)
75 | }
76 |
77 | }
78 |
79 | public init(connectionIdentifier: Int) {
80 | isExclusiveTunnel = true
81 | identifier = connectionIdentifier
82 | }
83 |
84 | // MARK: Interface
85 |
86 | /// Set a new tunnel for the connection.
87 | func setNewTunnel(newTunnel: Tunnel) {
88 | tunnel = newTunnel
89 | if let t = tunnel {
90 | t.addConnection(self)
91 | }
92 | }
93 |
94 | /// Close the connection.
95 | public func closeConnection(direction: TunnelConnectionCloseDirection) {
96 | if direction != .None && direction != currentCloseDirection {
97 | currentCloseDirection = .All
98 | }
99 | else {
100 | currentCloseDirection = direction
101 | }
102 |
103 | guard let currentTunnel = tunnel where currentCloseDirection == .All else { return }
104 |
105 | if isExclusiveTunnel {
106 | currentTunnel.closeTunnel()
107 | }
108 | else {
109 | currentTunnel.dropConnection(self)
110 | tunnel = nil
111 | }
112 | }
113 |
114 | /// Abort the connection.
115 | public func abort(error: Int = 0) {
116 | savedData.clear()
117 | }
118 |
119 | /// Send data on the connection.
120 | public func sendData(data: NSData) {
121 | }
122 |
123 | /// Send data and the destination host and port on the connection.
124 | public func sendDataWithEndPoint(data: NSData, host: String, port: Int) {
125 | }
126 |
127 | /// Send a list of IP packets and their associated protocols on the connection.
128 | public func sendPackets(packets: [NSData], protocols: [NSNumber]) {
129 | }
130 |
131 | /// 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.
132 | public func suspend() {
133 | }
134 |
135 | /// Send an indication to the remote end of the connection that the caller is going to start reading more data from the connection.
136 | public func resume() {
137 | }
138 |
139 | /// Handle the "open completed" message sent by the SimpleTunnel server.
140 | public func handleOpenCompleted(resultCode: TunnelConnectionOpenResult, properties: [NSObject: AnyObject]) {
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/SimpleTunnelServices/FilterUtilities.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 | public class FilterUtilities {
41 |
42 | // MARK: Properties
43 |
44 | /// A reference to the SimpleTunnel user defaults.
45 | public static let defaults = NSUserDefaults(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 | public 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?.objectForKey("rules")?.objectForKey(hostname) as? [String: AnyObject] else {
56 | print("\(hostname) is set for NO RULES")
57 | return (.Allow, hostname, [:])
58 | }
59 |
60 | guard let ruleTypeInt = hostNameRule["kRule"] as? Int,
61 | 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 | public class func getFlowHostname(flow: NEFilterFlow) -> String {
69 | guard let browserFlow : NEFilterBrowserFlow = flow as? NEFilterBrowserFlow,
70 | url = browserFlow.URL,
71 | hostname = url.host
72 | where flow is NEFilterBrowserFlow
73 | else { return "" }
74 | return hostname
75 | }
76 |
77 | /// Download a fresh set of rules from the rules server.
78 | public class func fetchRulesFromServer(serverAddress: String?) {
79 | print("fetch rules called")
80 |
81 | guard serverAddress != nil else { return }
82 | print("Fetching rules from \(serverAddress)")
83 |
84 | guard let infoURL = NSURL(string: "http://\(serverAddress!)/rules/") else { return }
85 | print("Rules url is \(infoURL)")
86 |
87 | let content: String
88 | do {
89 | content = try String(contentsOfURL: infoURL, encoding: NSUTF8StringEncoding)
90 | }
91 | catch {
92 | print("Failed to fetch the rules from \(infoURL)")
93 | return
94 | }
95 |
96 | let contentArray = content.componentsSeparatedByString("
")
97 | print("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.componentsSeparatedByString(" ")
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,
132 | "kRedirectKey" : redirectKey,
133 | "kRemediateKey" : remediateKey,
134 | "kRemediateButtonKey" : remediateButtonKey,
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) 2015 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) 2015 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: ErrorType {
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: NSData, 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: NSData, 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: NSOutputStream) -> Bool {
41 | var result = true
42 | var stopIndex = 0;
43 | for (chainIndex, record) in chain.enumerate() {
44 | let written = writeData(record.data, toStream: stream, startingAtOffset:record.offset)
45 | if written < 0 {
46 | result = false
47 | break
48 | }
49 | if written < (record.data.length - record.offset) {
50 | // Failed to write all of the remaining data in this blob, update the offset.
51 | chain[chainIndex] = (record.data, record.offset + written)
52 | stopIndex = chainIndex
53 | break
54 | }
55 | }
56 |
57 | if stopIndex == 0 {
58 | // All of the data was written.
59 | chain.removeAll(keepCapacity: false)
60 | }
61 | else if stopIndex > 0 {
62 | // We did not write all of the data, remove what was written.
63 | chain.removeRange(Range(start: 0, end: stopIndex))
64 | }
65 | return result
66 | }
67 |
68 | /// Remove all data from the list.
69 | func clear() {
70 | chain.removeAll(keepCapacity: false)
71 | }
72 | }
73 |
74 | /// A object containing a sockaddr_in6 structure.
75 | class SocketAddress6 {
76 |
77 | // MARK: Properties
78 |
79 | /// The sockaddr_in6 structure.
80 | var sin6: sockaddr_in6
81 |
82 | /// The IPv6 address as a string.
83 | var stringValue: String? {
84 | return withUnsafePointer(&sin6) { saToString(UnsafePointer($0)) }
85 | }
86 |
87 | // MARK: Initializers
88 |
89 | init() {
90 | sin6 = sockaddr_in6()
91 | sin6.sin6_len = __uint8_t(sizeof(sockaddr_in6))
92 | sin6.sin6_family = sa_family_t(AF_INET6)
93 | sin6.sin6_port = in_port_t(0)
94 | sin6.sin6_addr = in6addr_any
95 | sin6.sin6_scope_id = __uint32_t(0)
96 | sin6.sin6_flowinfo = __uint32_t(0)
97 | }
98 |
99 | convenience init(otherAddress: SocketAddress6) {
100 | self.init()
101 | sin6 = otherAddress.sin6
102 | }
103 |
104 | /// Set the IPv6 address from a string.
105 | func setFromString(str: String) -> Bool {
106 | return str.withCString({ cs in inet_pton(AF_INET6, cs, &sin6.sin6_addr) }) == 1
107 | }
108 |
109 | /// Set the port.
110 | func setPort(port: Int) {
111 | sin6.sin6_port = in_port_t(UInt16(port).bigEndian)
112 | }
113 | }
114 |
115 | /// An object containing a sockaddr_in structure.
116 | class SocketAddress {
117 |
118 | // MARK: Properties
119 |
120 | /// The sockaddr_in structure.
121 | var sin: sockaddr_in
122 |
123 | /// The IPv4 address in string form.
124 | var stringValue: String? {
125 | return withUnsafePointer(&sin) { saToString(UnsafePointer($0)) }
126 | }
127 |
128 | // MARK: Initializers
129 |
130 | init() {
131 | sin = sockaddr_in(sin_len:__uint8_t(sizeof(sockaddr_in.self)), 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)))
132 | }
133 |
134 | convenience init(otherAddress: SocketAddress) {
135 | self.init()
136 | sin = otherAddress.sin
137 | }
138 |
139 | /// Set the IPv4 address from a string.
140 | func setFromString(str: String) -> Bool {
141 | return str.withCString({ cs in inet_pton(AF_INET, cs, &sin.sin_addr) }) == 1
142 | }
143 |
144 | /// Set the port.
145 | func setPort(port: Int) {
146 | sin.sin_port = in_port_t(UInt16(port).bigEndian)
147 | }
148 |
149 | /// Increment the address by a given amount.
150 | func increment(amount: UInt32) {
151 | let networkAddress = sin.sin_addr.s_addr.byteSwapped + amount
152 | sin.sin_addr.s_addr = networkAddress.byteSwapped
153 | }
154 |
155 | /// Get the difference between this address and another address.
156 | func difference(otherAddress: SocketAddress) -> Int64 {
157 | return Int64(sin.sin_addr.s_addr.byteSwapped - otherAddress.sin.sin_addr.s_addr.byteSwapped)
158 | }
159 | }
160 |
161 | // MARK: Utility Functions
162 |
163 | /// Convert a sockaddr structure to a string.
164 | func saToString(sa: UnsafePointer) -> String? {
165 | var hostBuffer = [CChar](count: Int(NI_MAXHOST), repeatedValue:0)
166 | var portBuffer = [CChar](count: Int(NI_MAXSERV), repeatedValue:0)
167 |
168 | guard getnameinfo(sa, socklen_t(sa.memory.sa_len), &hostBuffer, socklen_t(hostBuffer.count), &portBuffer, socklen_t(portBuffer.count), NI_NUMERICHOST | NI_NUMERICSERV) == 0
169 | else { return nil }
170 |
171 | return String.fromCString(hostBuffer)
172 | }
173 |
174 | /// Write a blob of data to a stream starting from a particular offset.
175 | func writeData(data: NSData, toStream stream: NSOutputStream, startingAtOffset offset: Int) -> Int {
176 | var written = 0
177 | var currentOffset = offset
178 | while stream.hasSpaceAvailable && currentOffset < data.length {
179 |
180 | let writeResult = stream.write(UnsafePointer(data.bytes) + currentOffset, maxLength: data.length - currentOffset)
181 | guard writeResult >= 0 else { return writeResult }
182 |
183 | written += writeResult
184 | currentOffset += writeResult
185 | }
186 |
187 | return written
188 | }
189 |
190 | /// Create a SimpleTunnel protocol message dictionary.
191 | public func createMessagePropertiesForConnection(connectionIdentifier: Int, commandType: TunnelCommand, extraProperties: [String: AnyObject] = [:]) -> [String: AnyObject] {
192 | // Start out with the "extra properties" that the caller specified.
193 | var properties = extraProperties
194 |
195 | // Add in the standard properties common to all messages.
196 | properties[TunnelMessageKey.Identifier.rawValue] = connectionIdentifier
197 | properties[TunnelMessageKey.Command.rawValue] = commandType.rawValue
198 |
199 | return properties
200 | }
201 |
202 | /// Keys in the tunnel server configuration plist.
203 | public enum SettingsKey: String {
204 | case IPv4 = "IPv4"
205 | case DNS = "DNS"
206 | case Proxies = "Proxies"
207 | case Pool = "Pool"
208 | case StartAddress = "StartAddress"
209 | case EndAddress = "EndAddress"
210 | case Servers = "Servers"
211 | case SearchDomains = "SearchDomains"
212 | case Address = "Address"
213 | case Netmask = "Netmask"
214 | case Routes = "Routes"
215 | }
216 |
217 | /// Get a value from a plist given a list of keys.
218 | public func getValueFromPlist(plist: [NSObject: AnyObject], keyArray: [SettingsKey]) -> AnyObject? {
219 | var subPlist = plist
220 | for (index, key) in keyArray.enumerate() {
221 | if index == keyArray.count - 1 {
222 | return subPlist[key.rawValue]
223 | }
224 | else if let subSubPlist = subPlist[key.rawValue] as? [NSObject: AnyObject] {
225 | subPlist = subSubPlist
226 | }
227 | else {
228 | break
229 | }
230 | }
231 |
232 | return nil
233 | }
234 |
235 | /// Create a new range by incrementing the start of the given range by a given ammount.
236 | func rangeByMovingStartOfRange(range: Range, byCount: Int) -> Range {
237 | return Range(start: range.startIndex + byCount, end: range.endIndex)
238 | }
239 |
--------------------------------------------------------------------------------
/tunnel_server/AddressPool.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 Apple Inc. All Rights Reserved.
3 | See LICENSE.txt for this sample’s licensing information
4 |
5 | Abstract:
6 | This file contains the AddressPool class. The AddressPool class is used to manage a pool of IP addresses.
7 | */
8 |
9 | import Foundation
10 |
11 | /// An object that contains a pool of IP addresses to assign to tunnel clients.
12 | class AddressPool {
13 |
14 | // MARK: Properties
15 |
16 | /// The start address of the pool.
17 | let baseAddress: SocketAddress
18 |
19 | /// The number of addresses in the pool.
20 | var size: UInt64 = 0
21 |
22 | /// A list of flags indicating which addresses in the pool are currently allocated to clients.
23 | var inUseMask: [Bool]
24 |
25 | /// A dispatch queue for serializing access to the pool.
26 | let queue: dispatch_queue_t
27 |
28 | // MARK: Initializers
29 |
30 | init(startAddress: String, endAddress: String) {
31 | baseAddress = SocketAddress()
32 | inUseMask = [Bool](count: 0, repeatedValue: false)
33 | queue = dispatch_queue_create("AddressPoolQueue", nil)
34 |
35 | let start = SocketAddress()
36 | let end = SocketAddress()
37 |
38 | // Verify that the address pool is specified correctly.
39 |
40 | guard start.setFromString(startAddress) &&
41 | end.setFromString(endAddress) &&
42 | start.sin.sin_family == end.sin.sin_family
43 | else { return }
44 |
45 | guard start.sin.sin_family == sa_family_t(AF_INET) else {
46 | print("IPv6 is not currently supported")
47 | return
48 | }
49 | guard (start.sin.sin_addr.s_addr & 0xffff) == (end.sin.sin_addr.s_addr & 0xffff) else {
50 | print("start address (\(startAddress)) is not in the same class B network as end address (\(endAddress)) ")
51 | return
52 | }
53 |
54 | let difference = end.difference(start)
55 | guard difference >= 0 else {
56 | print("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](count: Int(size), repeatedValue: false)
63 | }
64 |
65 | /// Allocate an address from the pool.
66 | func allocateAddress() -> String? {
67 | var result: String?
68 |
69 | dispatch_sync(queue) {
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.enumerate() {
74 | if !inUse {
75 | address.increment(UInt32(index))
76 | self.inUseMask[index] = true
77 | result = address.stringValue
78 | break
79 | }
80 | }
81 | }
82 |
83 | print("Allocated address \(result)")
84 | return result
85 | }
86 |
87 | /// Deallocate an address in the pool.
88 | func deallocateAddress(addrString: String) {
89 | dispatch_sync(queue) {
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 | }
--------------------------------------------------------------------------------
/tunnel_server/ServerConfiguration.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 = NSInputStream(fileAtPath: path) else {
36 | print("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 NSPropertyListSerialization.propertyListWithStream(fileStream, options: .MutableContainers, format: nil) as! [String: AnyObject]
45 | }
46 | catch {
47 | print("Failed to read the configuration from \(path): \(error)")
48 | return false
49 | }
50 |
51 | guard let startAddress = getValueFromPlist(newConfiguration, keyArray: [.IPv4, .Pool, .StartAddress]) as? String else {
52 | print("Missing v4 start address")
53 | return false
54 | }
55 | guard let endAddress = getValueFromPlist(newConfiguration, keyArray: [.IPv4, .Pool, .EndAddress]) as? String else {
56 | print("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 var IPv4Dictionary = newConfiguration[SettingsKey.IPv4.rawValue] as? [NSObject: AnyObject] {
64 | IPv4Dictionary.removeValueForKey(SettingsKey.Pool.rawValue)
65 | newConfiguration[SettingsKey.IPv4.rawValue] = IPv4Dictionary
66 | }
67 |
68 | if !newConfiguration.keys.contains({ $0 == SettingsKey.DNS.rawValue }) {
69 | // The configuration does not specify any DNS configuration, so get the current system default resolver.
70 | let (DNSServers, DNSSearchDomains) = ServerConfiguration.copyDNSConfigurationFromSystem()
71 |
72 | newConfiguration[SettingsKey.DNS.rawValue] = [
73 | SettingsKey.Servers.rawValue: DNSServers,
74 | SettingsKey.SearchDomains.rawValue: DNSSearchDomains
75 | ]
76 | }
77 |
78 | configuration = newConfiguration
79 |
80 | return true
81 | }
82 |
83 | /// Copy the default resolver configuration from the system on which the server is running.
84 | class func copyDNSConfigurationFromSystem() -> ([String], [String]) {
85 | let globalDNSKey = SCDynamicStoreKeyCreateNetworkGlobalEntity(kCFAllocatorDefault, kSCDynamicStoreDomainState, kSCEntNetDNS)
86 | var DNSServers = [String]()
87 | var DNSSearchDomains = [String]()
88 |
89 | // The default resolver configuration can be obtained from State:/Network/Global/DNS in the dynamic store.
90 |
91 | if let globalDNS = SCDynamicStoreCopyValue(nil, globalDNSKey) as? [NSObject: AnyObject],
92 | servers = globalDNS[kSCPropNetDNSServerAddresses as String] as? [String]
93 | {
94 | if let searchDomains = globalDNS[kSCPropNetDNSSearchDomains as String] as? [String] {
95 | DNSSearchDomains = searchDomains
96 | }
97 | DNSServers = servers
98 | }
99 |
100 | return (DNSServers, DNSSearchDomains)
101 | }
102 | }
--------------------------------------------------------------------------------
/tunnel_server/ServerConnection.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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, NSStreamDelegate {
13 |
14 | // MARK: Properties
15 |
16 | /// The stream used to read network data from the connection.
17 | var readStream: NSInputStream?
18 |
19 | /// The stream used to write network data to the connection.
20 | var writeStream: NSOutputStream?
21 |
22 | // MARK: Interface
23 |
24 | /// Open the connection to a host and port.
25 | func open(host: String, port: Int) -> Bool {
26 | print("Connection \(identifier) connecting to \(host):\(port)")
27 |
28 | NSStream.getStreamsToHostWithName(host, port: port, inputStream: &readStream, outputStream: &writeStream)
29 |
30 | guard let newReadStream = readStream, newWriteStream = writeStream else {
31 | return false
32 | }
33 |
34 | for stream in [newReadStream, newWriteStream] {
35 | stream.delegate = self
36 | stream.open()
37 | stream.scheduleInRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
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 where isClosedForWrite && savedData.isEmpty {
50 | if let error = stream.streamError {
51 | print("Connection \(identifier) write stream error: \(error)")
52 | }
53 |
54 | stream.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
55 | stream.close()
56 | stream.delegate = nil
57 | writeStream = nil
58 | }
59 |
60 | if let stream = readStream where isClosedForRead {
61 | if let error = stream.streamError {
62 | print("Connection \(identifier) read stream error: \(error)")
63 | }
64 |
65 | stream.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
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.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
82 | }
83 | }
84 |
85 | /// Start reading from the connection.
86 | override func resume() {
87 | if let stream = readStream {
88 | stream.scheduleInRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
89 | }
90 | }
91 |
92 | /// Send data over the connection.
93 | override func sendData(data: NSData) {
94 | guard let stream = writeStream else { return }
95 | var written = 0
96 |
97 | if savedData.isEmpty {
98 | written = writeData(data, toStream: stream, startingAtOffset: 0)
99 |
100 | if written < data.length {
101 | // We could not write all of the data to the connection. Tell the client to stop reading data for this connection.
102 | stream.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
103 | tunnel?.sendSuspendForConnection(identifier)
104 | }
105 | }
106 |
107 | if written < data.length {
108 | savedData.append(data, offset: written)
109 | }
110 | }
111 |
112 | // MARK: NSStreamDelegate
113 |
114 | /// Handle an event on a stream.
115 | func stream(aStream: NSStream, handleEvent eventCode: NSStreamEvent) {
116 | switch aStream {
117 |
118 | case writeStream!:
119 | switch eventCode {
120 | case [.HasSpaceAvailable]:
121 | if !savedData.isEmpty {
122 | if savedData.writeToStream(writeStream!) {
123 | writeStream?.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
124 | if isClosedForWrite {
125 | closeConnection(.Write)
126 | }
127 | else {
128 | tunnel?.sendResumeForConnection(identifier)
129 | }
130 | }
131 | }
132 | else {
133 | writeStream?.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
134 | }
135 |
136 | case [.EndEncountered]:
137 | tunnel?.sendCloseType(.Read, forConnection: identifier)
138 | closeConnection(.Write)
139 |
140 | case [.ErrorOccurred]:
141 | tunnel?.sendCloseType(.All, forConnection: identifier)
142 | abort()
143 |
144 | default:
145 | break
146 | }
147 |
148 | case readStream!:
149 | switch eventCode {
150 | case [.HasBytesAvailable]:
151 | if let stream = readStream {
152 | while stream.hasBytesAvailable {
153 | var readBuffer = [UInt8](count: 8192, repeatedValue: 0)
154 | let bytesRead = stream.read(&readBuffer, maxLength: readBuffer.count)
155 |
156 | if bytesRead < 0 {
157 | abort()
158 | break
159 | }
160 |
161 | if bytesRead == 0 {
162 | tunnel?.sendCloseType(.Write, forConnection: identifier)
163 | closeConnection(.Read)
164 | break
165 | }
166 |
167 | let readData = NSData(bytes: readBuffer, length: bytesRead)
168 | tunnel?.sendData(readData, forConnection: identifier)
169 | }
170 | }
171 |
172 | case [.EndEncountered]:
173 | tunnel?.sendCloseType(.Write, forConnection: identifier)
174 | closeConnection(.Read)
175 |
176 | case [.ErrorOccurred]:
177 | if let serverTunnel = tunnel as? ServerTunnel {
178 | serverTunnel.sendOpenResultForConnection(identifier, resultCode: .Timeout)
179 | serverTunnel.sendCloseType(.All, forConnection: identifier)
180 | abort()
181 | }
182 |
183 | case [.OpenCompleted]:
184 | if let serverTunnel = tunnel as? ServerTunnel {
185 | serverTunnel.sendOpenResultForConnection(identifier, resultCode: .Success)
186 | }
187 |
188 | default:
189 | break
190 | }
191 | default:
192 | break
193 | }
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/tunnel_server/ServerTunnel.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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, NSStreamDelegate {
14 |
15 | // MARK: Properties
16 |
17 | /// The stream used to read data from the tunnel TCP connection.
18 | var readStream: NSInputStream?
19 |
20 | /// The stream used to write data to the tunnel TCP connection.
21 | var writeStream: NSOutputStream?
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: NSInputStream, newWriteStream: NSOutputStream) {
38 | super.init()
39 | delegate = self
40 |
41 | for stream in [newReadStream, newWriteStream] {
42 | stream.delegate = self
43 | stream.open()
44 | stream.scheduleInRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
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) -> NSNetService {
54 | let service = NSNetService(domain:Tunnel.serviceDomain, type:Tunnel.serviceType, name: "", port: port)
55 |
56 | print("Starting network service on port \(port)")
57 |
58 | service.delegate = ServerTunnel.serviceDelegate
59 | service.publishWithOptions(NSNetServiceOptions.ListenForConnections)
60 | service.scheduleInRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
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)
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](count: Tunnel.packetSize, repeatedValue: 0)
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 = sizeof(UInt32.self) - 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.appendBytes(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 == sizeof(UInt32.self) {
102 | var totalLength: UInt32 = 0
103 | packetBuffer.getBytes(&totalLength, length: sizeofValue(totalLength))
104 |
105 | guard totalLength <= Tunnel.maximumMessageSize else { return false }
106 |
107 | // Compute the length of the payload.
108 | packetBytesRemaining = Int(totalLength) - sizeofValue(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) {
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
132 | ])
133 |
134 | if !sendMessage(properties) {
135 | print("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 | tunnelLayerNumber = properties[TunnelMessageKey.TunnelType.rawValue] as? Int,
143 | 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 | 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 | port = properties[TunnelMessageKey.Port.rawValue] as? NSNumber
157 | else { break }
158 | let newConnection = ServerConnection(connectionIdentifier: connectionIdentifier, parentTunnel: self)
159 | guard newConnection.open(host, port: port.integerValue) else {
160 | newConnection.closeConnection(.All)
161 | break
162 | }
163 |
164 | case .UDP:
165 | let _ = UDPServerConnection(connectionIdentifier: connectionIdentifier, parentTunnel: self)
166 | sendOpenResultForConnection(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: NSStream, handleEvent eventCode: NSStreamEvent) {
182 | switch aStream {
183 |
184 | case writeStream!:
185 | switch eventCode {
186 | case [.HasSpaceAvailable]:
187 | // Send any buffered data.
188 | if !savedData.isEmpty && savedData.writeToStream(writeStream!) {
189 | for connection in connections.values {
190 | connection.resume()
191 | }
192 | }
193 |
194 | case [.ErrorOccurred]:
195 | closeTunnel()
196 | delegate?.tunnelDidClose(self)
197 |
198 | default:
199 | break
200 | }
201 |
202 | case readStream!:
203 | var needCloseTunnel = false
204 | switch eventCode {
205 | case [.HasBytesAvailable]:
206 | needCloseTunnel = !handleBytesAvailable()
207 |
208 | case [.OpenCompleted]:
209 | delegate?.tunnelDidOpen(self)
210 |
211 | case [.ErrorOccurred], [.EndEncountered]:
212 | needCloseTunnel = true
213 |
214 | default:
215 | break
216 | }
217 |
218 | if needCloseTunnel {
219 | closeTunnel()
220 | delegate?.tunnelDidClose(self)
221 | }
222 |
223 | default:
224 | break
225 | }
226 |
227 | }
228 |
229 | // MARK: Tunnel
230 |
231 | /// Close the tunnel.
232 | override func closeTunnel() {
233 |
234 | if let stream = readStream {
235 | if let error = stream.streamError {
236 | print("Tunnel read stream error: \(error)")
237 | }
238 |
239 | let socketData = CFReadStreamCopyProperty(stream, kCFStreamPropertySocketNativeHandle) as? NSData
240 |
241 | stream.removeFromRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
242 | stream.close()
243 | stream.delegate = nil
244 | readStream = nil
245 |
246 | if let data = socketData {
247 | var socket: CFSocketNativeHandle = 0
248 | data.getBytes(&socket, length: sizeofValue(socket))
249 | close(socket)
250 | }
251 | }
252 |
253 | if let stream = writeStream {
254 | if let error = stream.streamError {
255 | print("Tunnel write stream error: \(error)")
256 | }
257 |
258 | stream.removeFromRunLoop(.mainRunLoop(), forMode: NSDefaultRunLoopMode)
259 | stream.close()
260 | stream.delegate = nil
261 | }
262 |
263 | super.closeTunnel()
264 | }
265 |
266 | /// Handle a message received from the client.
267 | override func handleMessage(commandType: TunnelCommand, properties: [String: AnyObject], connection: Connection?) -> Bool {
268 | var success = true
269 |
270 | switch commandType {
271 | case .Open:
272 | handleConnectionOpen(properties)
273 |
274 | default:
275 | print("Tunnel received an invalid command")
276 | success = false
277 | }
278 | return success
279 | }
280 |
281 | /// Write data to the tunnel connection.
282 | override func writeDataToTunnel(data: NSData, startingAtOffset: Int) -> Int {
283 | guard let stream = writeStream else { return -1 }
284 | return writeData(data, toStream: stream, startingAtOffset:startingAtOffset)
285 | }
286 |
287 | // MARK: TunnelDelegate
288 |
289 | /// Handle the "tunnel open" event.
290 | func tunnelDidOpen(targetTunnel: Tunnel) {
291 | }
292 |
293 | /// Handle the "tunnel closed" event.
294 | func tunnelDidClose(targetTunnel: Tunnel) {
295 | }
296 | }
297 |
298 | /// An object that servers as the delegate for the network service published by the server.
299 | class ServerDelegate : NSObject, NSNetServiceDelegate {
300 |
301 | // MARK: NSNetServiceDelegate
302 |
303 | /// Handle the "failed to publish" event.
304 | func netService(sender: NSNetService, didNotPublish errorDict: [String : NSNumber]) {
305 | print("Failed to publish network service")
306 | exit(1)
307 | }
308 |
309 | /// Handle the "published" event.
310 | func netServiceDidPublish(sender: NSNetService) {
311 | print("Network service published successfully")
312 | }
313 |
314 | /// Handle the "new connection" event.
315 | func netService(sender: NSNetService, didAcceptConnectionWithInputStream inputStream: NSInputStream, outputStream: NSOutputStream) {
316 | print("Accepted a new connection")
317 | _ = ServerTunnel(newReadStream: inputStream, newWriteStream: outputStream)
318 | }
319 |
320 | /// Handle the "stopped" event.
321 | func netServiceDidStop(sender: NSNetService) {
322 | print("Network service stopped")
323 | exit(0)
324 | }
325 | }
326 |
327 |
--------------------------------------------------------------------------------
/tunnel_server/ServerTunnelConnection.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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: dispatch_source_t?
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
37 |
38 | let properties = createMessagePropertiesForConnection(identifier, commandType: .OpenResult, extraProperties: resultProperties)
39 |
40 | 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 | print("Failed to allocate a tunnel address")
49 | sendOpenResult(.Refused)
50 | return false
51 | }
52 |
53 | // Create the virtual interface and assign the address.
54 | guard setupVirtualInterface(address) else {
55 | print("Failed to set up the virtual interface")
56 | ServerTunnel.configuration.addressPool?.deallocateAddress(address)
57 | sendOpenResult(.InternalError)
58 | return false
59 | }
60 |
61 | tunnelAddress = address
62 |
63 | var response = createMessagePropertiesForConnection(identifier, commandType: .OpenResult)
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 | print("No IPv4 Settings available")
69 | sendOpenResult(.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] = tunnelAddress
76 | newIPv4Dictionary[SettingsKey.Netmask.rawValue] = "255.255.255.255"
77 | personalized[SettingsKey.IPv4.rawValue] = newIPv4Dictionary
78 | response[TunnelMessageKey.Configuration.rawValue] = personalized
79 |
80 | // Send the personalized configuration along with the "open result" message.
81 | sendOpenResult(.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 | print("Failed to open a kernel control socket")
92 | return -1
93 | }
94 |
95 | let controlIdentifier = getUTUNControlIdentifier(utunSocket)
96 | guard controlIdentifier > 0 else {
97 | print("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(sizeof(sockaddr_ctl.self)), 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(&socketAddressControl) {
106 | connect(utunSocket, UnsafePointer($0), socklen_t(sizeofValue(socketAddressControl)))
107 | }
108 |
109 | if let errorString = String(UTF8String: strerror(errno)) where connectResult < 0 {
110 | print("Failed to create a utun interface: \(errorString)")
111 | close(utunSocket)
112 | return -1
113 | }
114 |
115 | return utunSocket
116 | }
117 |
118 | /// Get the name of a UTUN interface the associated socket.
119 | func getTUNInterfaceName(utunSocket: Int32) -> String? {
120 | var buffer = [Int8](count: Int(IFNAMSIZ), repeatedValue: 0)
121 | var bufferSize: socklen_t = socklen_t(buffer.count)
122 | let resultCode = getsockopt(utunSocket, SYSPROTO_CONTROL, getUTUNNameOption(), &buffer, &bufferSize)
123 | if let errorString = String(UTF8String: strerror(errno)) where resultCode < 0 {
124 | print("getsockopt failed while getting the utun interface name: \(errorString)")
125 | return nil
126 | }
127 | return String(UTF8String: &buffer)
128 | }
129 |
130 | /// Set up the UTUN interface, start reading packets.
131 | func setupVirtualInterface(address: String) -> Bool {
132 | let utunSocket = createTUNInterface()
133 | guard let interfaceName = getTUNInterfaceName(utunSocket)
134 | where utunSocket >= 0 &&
135 | setUTUNAddress(interfaceName, address)
136 | else { return false }
137 |
138 | startTunnelSource(utunSocket)
139 | utunName = interfaceName
140 | return true
141 | }
142 |
143 | /// Read packets from the UTUN interface.
144 | func readPackets() {
145 | guard let source = utunSource else { return }
146 | var packets = [NSData]()
147 | var protocols = [NSNumber]()
148 |
149 | // 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.
150 | var buffer = [UInt8](count: Tunnel.packetSize, repeatedValue:0)
151 | var protocolNumber: UInt32 = 0
152 | var iovecList = [ iovec(iov_base: &protocolNumber, iov_len: sizeofValue(protocolNumber)), iovec(iov_base: &buffer, iov_len: buffer.count) ]
153 | let iovecListPointer = UnsafeBufferPointer(start: &iovecList[0], count: iovecList.count)
154 | let utunSocket = Int32(dispatch_source_get_handle(source))
155 |
156 | repeat {
157 | let readCount = readv(utunSocket, iovecListPointer.baseAddress, Int32(iovecListPointer.count))
158 |
159 | guard readCount > 0 || errno == EAGAIN else {
160 | if let errorString = String(UTF8String: strerror(errno)) where readCount < 0 {
161 | print("Got an error on the utun socket: \(errorString)")
162 | }
163 | dispatch_source_cancel(source)
164 | break
165 | }
166 |
167 | guard readCount > 0 else { break }
168 |
169 | if protocolNumber.littleEndian == protocolNumber {
170 | protocolNumber = protocolNumber.byteSwapped
171 | }
172 | protocols.append(NSNumber(unsignedInt: protocolNumber))
173 | packets.append(NSData(bytes: &buffer, length: readCount - sizeofValue(protocolNumber)))
174 |
175 | // Buffer up packets so that we can include multiple packets per message. Once we reach a per-message maximum send a "packets" message.
176 | if packets.count == Tunnel.maximumPacketsPerMessage {
177 | tunnel?.sendPackets(packets, protocols: protocols, forConnection: identifier)
178 | packets = [NSData]()
179 | protocols = [NSNumber]()
180 | if isSuspended { break } // If the entire message could not be sent and the connection is suspended, stop reading packets.
181 | }
182 | } while true
183 |
184 | // If there are unsent packets left over, send them now.
185 | if packets.count > 0 {
186 | tunnel?.sendPackets(packets, protocols: protocols, forConnection: identifier)
187 | }
188 | }
189 |
190 | /// Start reading packets from the UTUN interface.
191 | func startTunnelSource(utunSocket: Int32) {
192 | guard setSocketNonBlocking(utunSocket) else { return }
193 | guard let newSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, UInt(utunSocket), 0, dispatch_get_main_queue()) else { return }
194 | dispatch_source_set_cancel_handler(newSource) {
195 | close(utunSocket)
196 | return
197 | }
198 |
199 | dispatch_source_set_event_handler(newSource) {
200 | self.readPackets()
201 | }
202 |
203 | dispatch_resume(newSource)
204 | utunSource = newSource
205 | }
206 |
207 | // MARK: Connection
208 |
209 | /// Abort the connection.
210 | override func abort(error: Int = 0) {
211 | super.abort(error)
212 | closeConnection(.All)
213 | }
214 |
215 | /// Close the connection.
216 | override func closeConnection(direction: TunnelConnectionCloseDirection) {
217 | super.closeConnection(direction)
218 |
219 | if currentCloseDirection == .All {
220 | if utunSource != nil {
221 | dispatch_source_cancel(utunSource!)
222 | }
223 | // De-allocate the address.
224 | if tunnelAddress != nil {
225 | ServerTunnel.configuration.addressPool?.deallocateAddress(tunnelAddress!)
226 | }
227 | utunName = nil
228 | }
229 | }
230 |
231 | /// Stop reading packets from the UTUN interface.
232 | override func suspend() {
233 | isSuspended = true
234 | if let source = utunSource {
235 | dispatch_suspend(source)
236 | }
237 | }
238 |
239 | /// Resume reading packets from the UTUN interface.
240 | override func resume() {
241 | isSuspended = false
242 | if let source = utunSource {
243 | dispatch_resume(source)
244 | readPackets()
245 | }
246 | }
247 |
248 | /// Write packets and associated protocols to the UTUN interface.
249 | override func sendPackets(packets: [NSData], protocols: [NSNumber]) {
250 | guard let source = utunSource else { return }
251 | let utunSocket = Int32(dispatch_source_get_handle(source))
252 |
253 | for (index, packet) in packets.enumerate() {
254 | guard index < protocols.count else { break }
255 |
256 | var protocolNumber = protocols[index].unsignedIntValue.bigEndian
257 |
258 | let buffer = UnsafeMutablePointer(packet.bytes)
259 | var iovecList = [ iovec(iov_base: &protocolNumber, iov_len: sizeofValue(protocolNumber)), iovec(iov_base: buffer, iov_len: packet.length) ]
260 |
261 | let writeCount = writev(utunSocket, &iovecList, Int32(iovecList.count))
262 | if writeCount < 0 {
263 | if let errorString = String(UTF8String: strerror(errno)) {
264 | print("Got an error while writing to utun: \(errorString)")
265 | }
266 | }
267 | else if writeCount < packet.length + sizeofValue(protocolNumber) {
268 | print("Wrote \(writeCount) bytes of a \(packet.length) byte packet to utun")
269 | }
270 | }
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/tunnel_server/ServerUtils.h:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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) 2015 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) 2015 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: dispatch_source_t?
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 responseSource != nil {
31 | dispatch_source_cancel(responseSource!)
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).memory
40 |
41 | switch Int32(socketAddress.sa_family) {
42 | case AF_INET:
43 | var socketAddressInet = UnsafePointer(socketAddressPointer).memory
44 | let length = Int(INET_ADDRSTRLEN) + 2
45 | var buffer = [CChar](count: length, repeatedValue: 0)
46 | let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length))
47 | let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped)
48 | return (String.fromCString(hostCString)!, port)
49 |
50 | case AF_INET6:
51 | var socketAddressInet6 = UnsafePointer(socketAddressPointer).memory
52 | let length = Int(INET6_ADDRSTRLEN) + 2
53 | var buffer = [CChar](count: length, repeatedValue: 0)
54 | let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length))
55 | let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped)
56 | return (String.fromCString(hostCString)!, port)
57 |
58 | default:
59 | return nil
60 | }
61 | }
62 |
63 | /// Create a UDP socket
64 | func createSocketWithAddressFamilyFromAddress(address: String) -> Bool {
65 | var sin = sockaddr_in()
66 | var sin6 = sockaddr_in6()
67 | var newSocket: Int32 = -1
68 |
69 | if address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 {
70 | // IPv6 peer.
71 | newSocket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP)
72 | addressFamily = AF_INET6
73 | }
74 | else if address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 {
75 | // IPv4 peer.
76 | newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
77 | addressFamily = AF_INET
78 | }
79 |
80 | guard newSocket > 0 else { return false }
81 |
82 | guard let newResponseSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, UInt(newSocket), 0, dispatch_get_main_queue()) else {
83 | close(newSocket)
84 | return false
85 | }
86 |
87 | dispatch_source_set_cancel_handler(newResponseSource) {
88 | print("closing udp socket for connection \(self.identifier)")
89 | let UDPSocket = Int32(dispatch_source_get_handle(newResponseSource))
90 | close(UDPSocket)
91 | }
92 |
93 | dispatch_source_set_event_handler(newResponseSource) {
94 | guard let source = self.responseSource else { return }
95 |
96 | var socketAddress = sockaddr_storage()
97 | var socketAddressLength = socklen_t(sizeof(sockaddr_storage.self))
98 | let response = [UInt8](count: 4096, repeatedValue: 0)
99 | let UDPSocket = Int32(dispatch_source_get_handle(source))
100 |
101 | let bytesRead = withUnsafeMutablePointer(&socketAddress) {
102 | recvfrom(UDPSocket, UnsafeMutablePointer(response), response.count, 0, UnsafeMutablePointer($0), &socketAddressLength)
103 | }
104 |
105 | guard bytesRead >= 0 else {
106 | if let errorString = String(UTF8String: strerror(errno)) {
107 | print("recvfrom failed: \(errorString)")
108 | }
109 | self.closeConnection(.All)
110 | return
111 | }
112 |
113 | guard bytesRead > 0 else {
114 | print("recvfrom returned EOF")
115 | self.closeConnection(.All)
116 | return
117 | }
118 |
119 | guard let endpoint = withUnsafePointer(&socketAddress, { self.getEndpointFromSocketAddress(UnsafePointer($0)) }) else {
120 | print("Failed to get the address and port from the socket address received from recvfrom")
121 | self.closeConnection(.All)
122 | return
123 | }
124 |
125 | let responseDatagram = NSData(bytes: UnsafePointer(response), length: bytesRead)
126 | print("UDP connection id \(self.identifier) received = \(bytesRead) bytes from host = \(endpoint.host) port = \(endpoint.port)")
127 | self.tunnel?.sendDataWithEndPoint(responseDatagram, forConnection: self.identifier, host: endpoint.host, port: endpoint.port)
128 | }
129 |
130 | dispatch_resume(newResponseSource)
131 | responseSource = newResponseSource
132 |
133 | return true
134 | }
135 |
136 | /// Send a datagram to a given host and port.
137 | override func sendDataWithEndPoint(data: NSData, host: String, port: Int) {
138 |
139 | if responseSource == nil {
140 | guard createSocketWithAddressFamilyFromAddress(host) else {
141 | print("UDP ServerConnection initialization failed.")
142 | return
143 | }
144 | }
145 |
146 | guard let source = responseSource else { return }
147 | let UDPSocket = Int32(dispatch_source_get_handle(source))
148 | let sent: Int
149 |
150 | switch addressFamily {
151 | case AF_INET:
152 | let serverAddress = SocketAddress()
153 | guard serverAddress.setFromString(host) else {
154 | print("Failed to convert \(host) into an IPv4 address")
155 | return
156 | }
157 | serverAddress.setPort(port)
158 |
159 | sent = withUnsafePointer(&serverAddress.sin) {
160 | sendto(UDPSocket, data.bytes, data.length, 0, UnsafePointer($0), socklen_t(serverAddress.sin.sin_len))
161 | }
162 |
163 | case AF_INET6:
164 | let serverAddress = SocketAddress6()
165 | guard serverAddress.setFromString(host) else {
166 | print("Failed to convert \(host) into an IPv6 address")
167 | return
168 | }
169 | serverAddress.setPort(port)
170 |
171 | sent = withUnsafePointer(&serverAddress.sin6) {
172 | sendto(UDPSocket, data.bytes, data.length, 0, UnsafePointer($0), socklen_t(serverAddress.sin6.sin6_len))
173 | }
174 |
175 | default:
176 | return
177 | }
178 |
179 | guard sent > 0 else {
180 | if let errorString = String(UTF8String: strerror(errno)) {
181 | print("UDP connection id \(identifier) failed to send data to host = \(host) port \(port). error = \(errorString)")
182 | }
183 | closeConnection(.All)
184 | return
185 | }
186 |
187 | if sent == data.length {
188 | // Success
189 | print("UDP connection id \(identifier) sent \(data.length) bytes to host = \(host) port \(port)")
190 | }
191 | }
192 |
193 | /// Close the connection.
194 | override func closeConnection(direction: TunnelConnectionCloseDirection) {
195 | super.closeConnection(direction)
196 |
197 | if let source = responseSource where isClosedForWrite && isClosedForRead {
198 | dispatch_source_cancel(source)
199 | responseSource = nil
200 | }
201 | }
202 | }
203 |
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/tunnel_server/config.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IPv4
6 |
7 | Pool
8 |
9 | EndAddress
10 | 192.168.10.19
11 | StartAddress
12 | 192.168.10.10
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tunnel_server/main.swift:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, UInt(SIGINT), 0, dispatch_get_main_queue())
13 |
14 | /// Dispatch source to catch and handle SIGTERM
15 | let termSignalSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, UInt(SIGTERM), 0, dispatch_get_main_queue())
16 |
17 | /// Basic sanity check of the parameters.
18 | if Process.arguments.count < 3 {
19 | print("Usage: \(Process.arguments[0]) ")
20 | exit(1)
21 | }
22 |
23 | func ignore(_: Int32) {
24 | }
25 | signal(SIGTERM, ignore)
26 | signal(SIGINT, ignore)
27 |
28 | let portString = Process.arguments[1]
29 | let configurationPath = Process.arguments[2]
30 | let networkService: NSNetService
31 |
32 | // Initialize the server.
33 |
34 | if !ServerTunnel.initializeWithConfigurationFile(configurationPath) {
35 | exit(1)
36 | }
37 |
38 | if let portNumber = Int(portString) {
39 | networkService = ServerTunnel.startListeningOnPort(Int32(portNumber))
40 | }
41 | else {
42 | print("Invalid port: \(portString)")
43 | exit(1)
44 | }
45 |
46 | // Set up signal handling.
47 |
48 | dispatch_source_set_event_handler(interruptSignalSource) {
49 | networkService.stop()
50 | return
51 | }
52 | dispatch_resume(interruptSignalSource)
53 |
54 | dispatch_source_set_event_handler(termSignalSource) {
55 | networkService.stop()
56 | return
57 | }
58 | dispatch_resume(termSignalSource)
59 |
60 | NSRunLoop.mainRunLoop().run()
--------------------------------------------------------------------------------
/tunnel_server/tunnel_server-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2015 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 |
--------------------------------------------------------------------------------