From 5bf7813d56f51676b0bc2db27507ec1af5da5c33 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Fri, 24 Aug 2018 11:57:09 +0200 Subject: [PATCH 1/3] Forward compound SessionReply to delegate Improves extensibility. --- .../AppExtension/TunnelKitProvider.swift | 22 ++++++++--------- .../Sources/Core/SessionProxy+PushReply.swift | 24 ++++++++++++++++++- TunnelKit/Sources/Core/SessionProxy.swift | 15 +++--------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index d4362038..7706ab44 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -451,18 +451,18 @@ extension TunnelKitProvider: SessionProxyDelegate { // MARK: SessionProxyDelegate (tunnel queue) /// :nodoc: - public func sessionDidStart(_ proxy: SessionProxy, remoteAddress: String, address: String, gatewayAddress: String, dnsServers: [String]) { + public func sessionDidStart(_ proxy: SessionProxy, remoteAddress: String, reply: SessionReply) { reasserting = false log.info("Session did start") log.info("Returned ifconfig parameters:") - log.info("\tTunnel: \(remoteAddress)") - log.info("\tOwn address: \(address)") - log.info("\tGateway: \(gatewayAddress)") - log.info("\tDNS: \(dnsServers)") + log.info("\tRemote: \(remoteAddress)") + log.info("\tLocal: \(reply.address)/\(reply.addressMask)") + log.info("\tGateway: \(reply.gatewayAddress)") + log.info("\tDNS: \(reply.dnsServers)") - bringNetworkUp(tunnel: remoteAddress, vpn: address, gateway: gatewayAddress, dnsServers: dnsServers) { (error) in + bringNetworkUp(remoteAddress: remoteAddress, reply: reply) { (error) in if let error = error { log.error("Failed to configure tunnel: \(error)") self.pendingStartHandler?(error) @@ -489,19 +489,19 @@ extension TunnelKitProvider: SessionProxyDelegate { socket?.shutdown() } - private func bringNetworkUp(tunnel: String, vpn: String, gateway: String, dnsServers: [String], completionHandler: @escaping (Error?) -> Void) { + private func bringNetworkUp(remoteAddress: String, reply: SessionReply, completionHandler: @escaping (Error?) -> Void) { // route all traffic to VPN let defaultRoute = NEIPv4Route.default() - defaultRoute.gatewayAddress = gateway + defaultRoute.gatewayAddress = reply.gatewayAddress - let ipv4Settings = NEIPv4Settings(addresses: [vpn], subnetMasks: ["255.255.255.255"]) + let ipv4Settings = NEIPv4Settings(addresses: [reply.address], subnetMasks: [reply.addressMask]) ipv4Settings.includedRoutes = [defaultRoute] ipv4Settings.excludedRoutes = [] - let dnsSettings = NEDNSSettings(servers: dnsServers) + let dnsSettings = NEDNSSettings(servers: reply.dnsServers) - let newSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnel) + let newSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress) newSettings.ipv4Settings = ipv4Settings newSettings.dnsSettings = dnsSettings diff --git a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift index ce826a6c..2e55538f 100644 --- a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift +++ b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift @@ -37,8 +37,27 @@ import Foundation +/// Represents the reply of a successful session start. +public protocol SessionReply { + + /// The obtained address. + var address: String { get } + + /// The obtained address mask. + var addressMask: String { get } + + /// The address of the default gateway. + var gatewayAddress: String { get } + + /// The DNS servers set up for this session. + var dnsServers: [String] { get } +} + extension SessionProxy { - struct PushReply { + + // XXX: parsing is very optimistic + + struct PushReply: SessionReply { private static let ifconfigRegexp = try! NSRegularExpression(pattern: "ifconfig [\\d\\.]+ [\\d\\.]+", options: []) private static let dnsRegexp = try! NSRegularExpression(pattern: "dhcp-option DNS [\\d\\.]+", options: []) @@ -49,6 +68,8 @@ extension SessionProxy { let address: String + let addressMask: String + let gatewayAddress: String let dnsServers: [String] @@ -110,6 +131,7 @@ extension SessionProxy { } address = addresses[1] + addressMask = "255.255.255.255" gatewayAddress = addresses[2] self.dnsServers = dnsServers self.authToken = authToken diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index fc05fcf3..7191a480 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -55,11 +55,9 @@ public protocol SessionProxyDelegate: class { Called after starting a session. - Parameter remoteAddress: The address of the VPN server. - - Parameter address: The obtained address. - - Parameter gatewayAddress: The address of the gateway. - - Parameter dnsServers: The DNS servers set up for this session. + - Parameter reply: The compound `SessionReply` containing tunnel settings. */ - func sessionDidStart(_: SessionProxy, remoteAddress: String, address: String, gatewayAddress: String, dnsServers: [String]) + func sessionDidStart(_: SessionProxy, remoteAddress: String, reply: SessionReply) /** Called after stopping a session. @@ -905,14 +903,7 @@ public class SessionProxy { guard let remoteAddress = link?.remoteAddress else { fatalError("Could not resolve link remote address") } - - delegate?.sessionDidStart( - self, - remoteAddress: remoteAddress, - address: reply.address, - gatewayAddress: reply.gatewayAddress, - dnsServers: reply.dnsServers - ) + delegate?.sessionDidStart(self, remoteAddress: remoteAddress, reply: reply) if let interval = configuration.keepAliveInterval { queue.asyncAfter(deadline: .now() + interval) { [weak self] in From b0d264889c9a4595d064e5991e45c3b4039b3ef5 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Fri, 24 Aug 2018 11:45:41 +0200 Subject: [PATCH 2/3] Extend PUSH_REPLY parsing - Topology - Routes Use the less confusing defaultGateway vs gatewayAddress. --- .../AppExtension/TunnelKitProvider.swift | 13 +- .../Sources/Core/SessionProxy+PushReply.swift | 147 ++++++++++++++++-- 2 files changed, 147 insertions(+), 13 deletions(-) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index 7706ab44..87136c1d 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -459,7 +459,7 @@ extension TunnelKitProvider: SessionProxyDelegate { log.info("Returned ifconfig parameters:") log.info("\tRemote: \(remoteAddress)") log.info("\tLocal: \(reply.address)/\(reply.addressMask)") - log.info("\tGateway: \(reply.gatewayAddress)") + log.info("\tGateway: \(reply.defaultGateway)") log.info("\tDNS: \(reply.dnsServers)") bringNetworkUp(remoteAddress: remoteAddress, reply: reply) { (error) in @@ -493,10 +493,17 @@ extension TunnelKitProvider: SessionProxyDelegate { // route all traffic to VPN let defaultRoute = NEIPv4Route.default() - defaultRoute.gatewayAddress = reply.gatewayAddress + defaultRoute.gatewayAddress = reply.defaultGateway + + var routes: [NEIPv4Route] = [defaultRoute] + for r in reply.routes { + let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) + ipv4Route.gatewayAddress = r.gateway ?? reply.defaultGateway + routes.append(ipv4Route) + } let ipv4Settings = NEIPv4Settings(addresses: [reply.address], subnetMasks: [reply.addressMask]) - ipv4Settings.includedRoutes = [defaultRoute] + ipv4Settings.includedRoutes = routes ipv4Settings.excludedRoutes = [] let dnsSettings = NEDNSSettings(servers: reply.dnsServers) diff --git a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift index 2e55538f..dabb4dfb 100644 --- a/TunnelKit/Sources/Core/SessionProxy+PushReply.swift +++ b/TunnelKit/Sources/Core/SessionProxy+PushReply.swift @@ -47,7 +47,10 @@ public protocol SessionReply { var addressMask: String { get } /// The address of the default gateway. - var gatewayAddress: String { get } + var defaultGateway: String { get } + + /// The additional routes. + var routes: [SessionProxy.Route] { get } /// The DNS servers set up for this session. var dnsServers: [String] { get } @@ -57,9 +60,42 @@ extension SessionProxy { // XXX: parsing is very optimistic + /// Represents a route in the routing table. + public struct Route { + + /// The destination host or subnet. + public let destination: String + + /// The address mask. + public let mask: String + + /// The address of the gateway (uses default gateway if not set). + public let gateway: String? + + fileprivate init(_ destination: String, _ mask: String?, _ gateway: String?) { + self.destination = destination + self.mask = mask ?? "255.255.255.255" + self.gateway = gateway + } + } + struct PushReply: SessionReply { + private enum Topology: String { + case net30 + + case p2p + + case subnet + } + + private static let topologyRegexp = try! NSRegularExpression(pattern: "topology (net30|p2p|subnet)", options: []) + private static let ifconfigRegexp = try! NSRegularExpression(pattern: "ifconfig [\\d\\.]+ [\\d\\.]+", options: []) + private static let gatewayRegexp = try! NSRegularExpression(pattern: "route-gateway [\\d\\.]+", options: []) + + private static let routeRegexp = try! NSRegularExpression(pattern: "route [\\d\\.]+( [\\d\\.]+){0,2}", options: []) + private static let dnsRegexp = try! NSRegularExpression(pattern: "dhcp-option DNS [\\d\\.]+", options: []) private static let authTokenRegexp = try! NSRegularExpression(pattern: "auth-token [a-zA-Z0-9/=+]+", options: []) @@ -70,7 +106,9 @@ extension SessionProxy { let addressMask: String - let gatewayAddress: String + let defaultGateway: String + + let routes: [Route] let dnsServers: [String] @@ -83,22 +121,84 @@ extension SessionProxy { return nil } - var ifconfigComponents: [String]? - var dnsServers = [String]() + var optTopologyComponents: [String]? + var optIfconfigComponents: [String]? + var optGatewayComponents: [String]? + + let address: String + let addressMask: String + let defaultGateway: String + var routes: [Route] = [] + var dnsServers: [String] = [] var authToken: String? var peerId: UInt32? + + // MARK: Routing + + PushReply.topologyRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in + guard let range = result?.range else { return } + + let match = (message as NSString).substring(with: range) + optTopologyComponents = match.components(separatedBy: " ") + } + guard let topologyComponents = optTopologyComponents, topologyComponents.count == 2 else { + throw SessionError.malformedPushReply + } + + // assumes "topology" to be always pushed to clients, even when not explicitly set (defaults to net30) + guard let topology = Topology(rawValue: topologyComponents[1]) else { + fatalError("Bad topology regexp, accepted unrecognized value: \(topologyComponents[1])") + } PushReply.ifconfigRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in guard let range = result?.range else { return } let match = (message as NSString).substring(with: range) - ifconfigComponents = match.components(separatedBy: " ") + optIfconfigComponents = match.components(separatedBy: " ") } - - guard let addresses = ifconfigComponents, addresses.count >= 2 else { + guard let ifconfigComponents = optIfconfigComponents, ifconfigComponents.count == 3 else { throw SessionError.malformedPushReply } + PushReply.gatewayRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in + guard let range = result?.range else { return } + + let match = (message as NSString).substring(with: range) + optGatewayComponents = match.components(separatedBy: " ") + } + + // + // excerpts from OpenVPN manpage + // + // "--ifconfig l rn": + // + // Set TUN/TAP adapter parameters. l is the IP address of the local VPN endpoint. For TUN devices in point-to-point mode, rn is the IP address of + // the remote VPN endpoint. For TAP devices, or TUN devices used with --topology subnet, rn is the subnet mask of the virtual network segment which + // is being created or connected to. + // + // "--topology mode": + // + // Note: Using --topology subnet changes the interpretation of the arguments of --ifconfig to mean "address netmask", no longer "local remote". + // + switch topology { + case .subnet: + + // default gateway required when topology is subnet + guard let gatewayComponents = optGatewayComponents, gatewayComponents.count == 2 else { + throw SessionError.malformedPushReply + } + address = ifconfigComponents[1] + addressMask = ifconfigComponents[2] + defaultGateway = gatewayComponents[1] + + default: + address = ifconfigComponents[1] + addressMask = "255.255.255.255" + defaultGateway = ifconfigComponents[2] + } + + // MARK: DNS + PushReply.dnsRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in guard let range = result?.range else { return } @@ -108,6 +208,32 @@ extension SessionProxy { dnsServers.append(dnsEntryComponents[2]) } + // MARK: Routes + + PushReply.routeRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in + guard let range = result?.range else { return } + + let match = (message as NSString).substring(with: range) + let routeEntryComponents = match.components(separatedBy: " ") + + let destination = routeEntryComponents[1] + let mask: String? + let gateway: String? + if routeEntryComponents.count > 2 { + mask = routeEntryComponents[2] + } else { + mask = nil + } + if routeEntryComponents.count > 3 { + gateway = routeEntryComponents[3] + } else { + gateway = defaultGateway + } + routes.append(Route(destination, mask, gateway)) + } + + // MARK: Authentication + PushReply.authTokenRegexp.enumerateMatches(in: message, options: [], range: NSMakeRange(0, message.count)) { (result, flags, _) in guard let range = result?.range else { return } @@ -130,10 +256,11 @@ extension SessionProxy { } } - address = addresses[1] - addressMask = "255.255.255.255" - gatewayAddress = addresses[2] + self.address = address + self.addressMask = addressMask + self.defaultGateway = defaultGateway self.dnsServers = dnsServers + self.routes = routes self.authToken = authToken self.peerId = peerId } From b0758412a030836e6724fbc7b5c005d9b5998a45 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Tue, 28 Aug 2018 16:43:54 +0200 Subject: [PATCH 3/3] Add PUSH_REPLY unit tests --- TunnelKit.xcodeproj/project.pbxproj | 4 ++ TunnelKitTests/PushTests.swift | 81 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 TunnelKitTests/PushTests.swift diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index 1eed649f..cf189675 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 0E1108B31F77B9F900A92462 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E1108B21F77B9F900A92462 /* Assets.xcassets */; }; 0E1108B61F77B9F900A92462 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E1108B41F77B9F900A92462 /* LaunchScreen.storyboard */; }; 0E245D6C2137F73600B012A2 /* CompressionFraming.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E245D6B2137F73600B012A2 /* CompressionFraming.h */; }; + 0E245D692135972800B012A2 /* PushTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E245D682135972800B012A2 /* PushTests.swift */; }; 0E3E0F212108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3E0F202108A8CC00B371C1 /* SessionProxy+PushReply.swift */; }; 0E3E0F222108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3E0F202108A8CC00B371C1 /* SessionProxy+PushReply.swift */; }; 0E85A25A202CC5AF0059E9F9 /* AppExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E85A259202CC5AE0059E9F9 /* AppExtensionTests.swift */; }; @@ -188,6 +189,7 @@ 0E1108B71F77B9F900A92462 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 0E17D7F91F730D9F009EE129 /* TunnelKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TunnelKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0E245D6B2137F73600B012A2 /* CompressionFraming.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CompressionFraming.h; sourceTree = ""; }; + 0E245D682135972800B012A2 /* PushTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushTests.swift; sourceTree = ""; }; 0E3251C51F95770D00C108D9 /* TunnelKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TunnelKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3E0F202108A8CC00B371C1 /* SessionProxy+PushReply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProxy+PushReply.swift"; sourceTree = ""; }; 0E6479DD212EAC96008E6888 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -312,6 +314,7 @@ 0EB2B45E20F0C098004233D7 /* EncryptionPerformanceTests.swift */, 0EB2B45220F0BB44004233D7 /* EncryptionTests.swift */, 0EB2B45820F0BD9A004233D7 /* LinkTests.swift */, + 0E245D682135972800B012A2 /* PushTests.swift */, 0EB2B45620F0BD16004233D7 /* RandomTests.swift */, 0EB2B45C20F0BF41004233D7 /* RawPerformanceTests.swift */, 0EB2B45A20F0BE4C004233D7 /* TestUtils.swift */, @@ -813,6 +816,7 @@ 0EB2B45520F0BB53004233D7 /* DataManipulationTests.swift in Sources */, 0EB2B45320F0BB44004233D7 /* EncryptionTests.swift in Sources */, 0EB2B45B20F0BE4C004233D7 /* TestUtils.swift in Sources */, + 0E245D692135972800B012A2 /* PushTests.swift in Sources */, 0EB2B46120F0C0A4004233D7 /* DataPathPerformanceTests.swift in Sources */, 0EB2B45F20F0C098004233D7 /* EncryptionPerformanceTests.swift in Sources */, 0EE7A7A120F664AC00B42E6A /* DataPathEncryptionTests.swift in Sources */, diff --git a/TunnelKitTests/PushTests.swift b/TunnelKitTests/PushTests.swift new file mode 100644 index 00000000..0914b888 --- /dev/null +++ b/TunnelKitTests/PushTests.swift @@ -0,0 +1,81 @@ +// +// PushTests.swift +// TunnelKitTests +// +// Created by Davide De Rosa on 8/24/18. +// Copyright (c) 2018 Davide De Rosa. All rights reserved. +// +// https://github.com/keeshux +// +// This file is part of TunnelKit. +// +// TunnelKit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// TunnelKit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with TunnelKit. If not, see . +// + +import XCTest +@testable import TunnelKit + +private extension SessionReply { + func debug() { + print("Address: \(address)") + print("Mask: \(addressMask)") + print("Gateway: \(defaultGateway)") + print("DNS: \(dnsServers)") + } +} + +class PushTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testNet30() { + let msg = "PUSH_REPLY,redirect-gateway def1,dhcp-option DNS 209.222.18.222,dhcp-option DNS 209.222.18.218,ping 10,comp-lzo no,route 10.5.10.1,topology net30,ifconfig 10.5.10.6 10.5.10.5,auth-token AUkQf/b3nj3L+CH4RJPP0Vuq8/gpntr7uPqzjQhncig=" + let reply = try! SessionProxy.PushReply(message: msg)! + reply.debug() + + XCTAssertEqual(reply.address, "10.5.10.6") + XCTAssertEqual(reply.addressMask, "255.255.255.255") + XCTAssertEqual(reply.defaultGateway, "10.5.10.5") + XCTAssertEqual(reply.dnsServers, ["209.222.18.222", "209.222.18.218"]) + } + + func testSubnet() { + let msg = "PUSH_REPLY,dhcp-option DNS 8.8.8.8,dhcp-option DNS 4.4.4.4,route-gateway 10.8.0.1,topology subnet,ping 10,ping-restart 120,ifconfig 10.8.0.2 255.255.255.0,peer-id 0" + let reply = try! SessionProxy.PushReply(message: msg)! + reply.debug() + + XCTAssertEqual(reply.address, "10.8.0.2") + XCTAssertEqual(reply.addressMask, "255.255.255.0") + XCTAssertEqual(reply.defaultGateway, "10.8.0.1") + XCTAssertEqual(reply.dnsServers, ["8.8.8.8", "4.4.4.4"]) + } + + func testRoute() { + let msg = "PUSH_REPLY,dhcp-option DNS 8.8.8.8,dhcp-option DNS 4.4.4.4,route-gateway 10.8.0.1,route 192.168.0.0 255.255.255.0 10.8.0.12,topology subnet,ping 10,ping-restart 120,ifconfig 10.8.0.2 255.255.255.0,peer-id 0" + let reply = try! SessionProxy.PushReply(message: msg)! + reply.debug() + + let route = reply.routes.first! + + XCTAssertEqual(route.destination, "192.168.0.0") + XCTAssertEqual(route.mask, "255.255.255.0") + XCTAssertEqual(route.gateway, "10.8.0.12") + } +}