diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe939f..524374f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Do not redirect all traffic to VPN unless `--redirect-gateway` specified. [#90](https://github.com/keeshux/tunnelkit/issues/90) + ### Fixed - SoftEther sends an incomplete PUSH_REPLY. [#86](https://github.com/keeshux/tunnelkit/issues/86) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index 96e61e7..b977ffc 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -176,6 +176,14 @@ extension TunnelKitProvider { sessionConfigurationBuilder.httpsProxy = proxy } sessionConfigurationBuilder.proxyBypassDomains = providerConfiguration[S.proxyBypassDomains] as? [String] + if let routingPoliciesStrings = providerConfiguration[S.routingPolicies] as? [String], !routingPoliciesStrings.isEmpty { + sessionConfigurationBuilder.routingPolicies = try routingPoliciesStrings.map { + guard let policy = SessionProxy.RoutingPolicy(rawValue: $0) else { + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.routingPolicies)] has a badly formed element") + } + return policy + } + } sessionConfiguration = sessionConfigurationBuilder.build() shouldDebug = providerConfiguration[S.debug] as? Bool ?? ConfigurationBuilder.defaults.shouldDebug @@ -259,6 +267,8 @@ extension TunnelKitProvider { static let proxyBypassDomains = "ProxyBypassDomains" + static let routingPolicies = "RoutingPolicies" + // MARK: Debugging static let debug = "Debug" @@ -471,6 +481,9 @@ extension TunnelKitProvider { if let proxyBypassDomains = sessionConfiguration.proxyBypassDomains { dict[S.proxyBypassDomains] = proxyBypassDomains } + if let routingPolicies = sessionConfiguration.routingPolicies { + dict[S.routingPolicies] = routingPolicies.map { $0.rawValue } + } // if let resolvedAddresses = resolvedAddresses { dict[S.resolvedAddresses] = resolvedAddresses @@ -559,6 +572,12 @@ extension TunnelKitProvider { if sessionConfiguration.randomizeEndpoint ?? false { log.info("\tRandomize endpoint: true") } + // FIXME: refine logging of other routing policies + if let routingPolicies = sessionConfiguration.routingPolicies { + log.info("\tDefault gateway: \(routingPolicies.map { $0.rawValue })") + } else { + log.info("\tDefault gateway: no") + } if let dnsServers = sessionConfiguration.dnsServers { log.info("\tDNS servers: \(dnsServers.maskedDescription)") } diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index 3a6a7be..e38ee0c 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -477,6 +477,7 @@ extension TunnelKitProvider: SessionProxyDelegate { } else { log.info("\tDNS: not configured") } + log.info("\tRouting policies: \(reply.options.routingPolicies?.maskedDescription ?? "not configured")") log.info("\tDomain: \(reply.options.searchDomain?.maskedDescription ?? "not configured")") if reply.options.httpProxy != nil || reply.options.httpsProxy != nil { @@ -492,7 +493,7 @@ extension TunnelKitProvider: SessionProxyDelegate { } } - bringNetworkUp(remoteAddress: remoteAddress, reply: reply) { (error) in + bringNetworkUp(remoteAddress: remoteAddress, configuration: proxy.configuration, reply: reply) { (error) in if let error = error { log.error("Failed to configure tunnel: \(error)") self.pendingStartHandler?(error) @@ -523,15 +524,20 @@ extension TunnelKitProvider: SessionProxyDelegate { socket?.shutdown() } - private func bringNetworkUp(remoteAddress: String, reply: SessionReply, completionHandler: @escaping (Error?) -> Void) { + private func bringNetworkUp(remoteAddress: String, configuration: SessionProxy.Configuration, reply: SessionReply, completionHandler: @escaping (Error?) -> Void) { + let routingPolicies = configuration.routingPolicies ?? reply.options.routingPolicies - // route all traffic to VPN var ipv4Settings: NEIPv4Settings? if let ipv4 = reply.options.ipv4 { - let defaultRoute = NEIPv4Route.default() - defaultRoute.gatewayAddress = ipv4.defaultGateway + var routes: [NEIPv4Route] = [] + + // route all traffic to VPN? + if routingPolicies?.contains(.IPv4) ?? false { + let defaultRoute = NEIPv4Route.default() + defaultRoute.gatewayAddress = ipv4.defaultGateway + routes.append(defaultRoute) + } - var routes: [NEIPv4Route] = [defaultRoute] for r in ipv4.routes { let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) ipv4Route.gatewayAddress = r.gateway @@ -545,10 +551,15 @@ extension TunnelKitProvider: SessionProxyDelegate { var ipv6Settings: NEIPv6Settings? if let ipv6 = reply.options.ipv6 { - let defaultRoute = NEIPv6Route.default() - defaultRoute.gatewayAddress = ipv6.defaultGateway + var routes: [NEIPv6Route] = [] + + // route all traffic to VPN? + if routingPolicies?.contains(.IPv6) ?? false { + let defaultRoute = NEIPv6Route.default() + defaultRoute.gatewayAddress = ipv6.defaultGateway + routes.append(defaultRoute) + } - var routes: [NEIPv6Route] = [defaultRoute] for r in ipv6.routes { let ipv6Route = NEIPv6Route(destinationAddress: r.destination, networkPrefixLength: r.prefixLength as NSNumber) ipv6Route.gatewayAddress = r.gateway @@ -556,7 +567,7 @@ extension TunnelKitProvider: SessionProxyDelegate { } ipv6Settings = NEIPv6Settings(addresses: [ipv6.address], networkPrefixLengths: [ipv6.addressPrefixLength as NSNumber]) - ipv6Settings?.includedRoutes = [defaultRoute] + ipv6Settings?.includedRoutes = routes ipv6Settings?.excludedRoutes = [] } diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index fbfe683..22de246 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -96,6 +96,8 @@ public class ConfigurationParser { static let proxyBypass = NSRegularExpression("^dhcp-option +PROXY_BYPASS +.+") + static let redirectGateway = NSRegularExpression("^redirect-gateway.*") + // MARK: Unsupported // static let fragment = NSRegularExpression("^fragment +\\d+") @@ -120,6 +122,26 @@ public class ConfigurationParser { case subnet } + private enum RedirectGateway: String { + case local + + case autolocal + + case def1 + + case bypassDHCP = "bypass-dhcp" + + case bypassDNS = "bypass-dns" + + case blockLocal = "block-local" + + case ipv4 + + case noIPv4 = "!ipv4" + + case ipv6 + } + /// Result of the parser. public struct Result { @@ -204,6 +226,7 @@ public class ConfigurationParser { var optHTTPProxy: Proxy? var optHTTPSProxy: Proxy? var optProxyBypass: [String]? + var optRedirectGateway: Set? log.verbose("Configuration file:") for line in lines { @@ -515,6 +538,22 @@ public class ConfigurationParser { optProxyBypass = $0 optProxyBypass?.removeFirst() } + Regex.redirectGateway.enumerateArguments(in: line) { + + // redirect IPv4 by default + optRedirectGateway = [.ipv4] + + for arg in $0 { + guard let opt = RedirectGateway(rawValue: arg) else { + continue + } + if opt == .noIPv4 { + optRedirectGateway?.remove(.ipv4) + } else { + optRedirectGateway?.insert(opt) + } + } + } // @@ -681,6 +720,11 @@ public class ConfigurationParser { sessionBuilder.httpsProxy = optHTTPSProxy sessionBuilder.proxyBypassDomains = optProxyBypass + // FIXME: only redirects all traffic until --redirect-gateway is properly interpreted + if let _ = optRedirectGateway { + sessionBuilder.routingPolicies = [.IPv4, .IPv6] + } + // return Result( diff --git a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift index de1419a..e90381a 100644 --- a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift +++ b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift @@ -146,6 +146,16 @@ extension SessionProxy { } } + /// Routing policy. + public enum RoutingPolicy: String, Codable { + + /// All IPv4 traffic goes through the VPN. + case IPv4 + + /// All IPv6 traffic goes through the VPN. + case IPv6 + } + /// :nodoc: private struct Fallback { static let cipher: Cipher = .aes128cbc @@ -238,6 +248,9 @@ extension SessionProxy { /// The list of domains not passing through the proxy. public var proxyBypassDomains: [String]? + /// Policies for redirecting traffic through the VPN gateway. + public var routingPolicies: [RoutingPolicy]? + /// :nodoc: public init() { } @@ -272,7 +285,8 @@ extension SessionProxy { searchDomain: searchDomain, httpProxy: httpProxy, httpsProxy: httpsProxy, - proxyBypassDomains: proxyBypassDomains + proxyBypassDomains: proxyBypassDomains, + routingPolicies: routingPolicies ) } @@ -369,6 +383,9 @@ extension SessionProxy { /// - Seealso: `SessionProxy.ConfigurationBuilder.proxyBypassDomains` public var proxyBypassDomains: [String]? + /// - Seealso: `SessionProxy.ConfigurationBuilder.routingPolicies` + public var routingPolicies: [RoutingPolicy]? + // MARK: Shortcuts /// :nodoc: @@ -422,6 +439,7 @@ extension SessionProxy.Configuration { builder.httpProxy = httpProxy builder.httpsProxy = httpsProxy builder.proxyBypassDomains = proxyBypassDomains + builder.routingPolicies = routingPolicies return builder } } diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index d769168..9f9afc7 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -79,7 +79,8 @@ public class SessionProxy { // MARK: Configuration - private let configuration: Configuration + /// The session base configuration. + public let configuration: Configuration /// The optional credentials. public var credentials: Credentials? diff --git a/TunnelKitTests/ConfigurationParserTests.swift b/TunnelKitTests/ConfigurationParserTests.swift index 7304490..683a735 100644 --- a/TunnelKitTests/ConfigurationParserTests.swift +++ b/TunnelKitTests/ConfigurationParserTests.swift @@ -27,8 +27,6 @@ import XCTest import TunnelKit class ConfigurationParserTests: XCTestCase { - let base: [String] = ["", "", "remote 1.2.3.4"] - override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. @@ -42,18 +40,18 @@ class ConfigurationParserTests: XCTestCase { // from lines func testCompression() throws { -// XCTAssertNotNil(try OptionsBundle.parsed(fromLines: base + ["comp-lzo"]).warning) - XCTAssertNil(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo"]).warning) - XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo no"])) - XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo yes"])) -// XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo yes"])) +// XCTAssertNotNil(try OptionsBundle.parsed(fromLines: ["comp-lzo"]).warning) + XCTAssertNil(try ConfigurationParser.parsed(fromLines: ["comp-lzo"]).warning) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: ["comp-lzo no"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: ["comp-lzo yes"])) +// XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: ["comp-lzo yes"])) - XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress"])) - XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress lzo"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: ["compress"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: ["compress lzo"])) } func testDHCPOption() throws { - let lines = base + [ + let lines = [ "dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com", @@ -73,8 +71,18 @@ class ConfigurationParserTests: XCTestCase { XCTAssertEqual(parsed.proxyBypassDomains, ["foo.com", "bar.org", "net.chat"]) } + func testRedirectGateway() throws { + var parsed: SessionProxy.Configuration + + parsed = try! ConfigurationParser.parsed(fromLines: []).configuration + XCTAssertEqual(parsed.routingPolicies, nil) + XCTAssertNotEqual(parsed.routingPolicies, []) + parsed = try! ConfigurationParser.parsed(fromLines: ["redirect-gateway ipv4 block-local"]).configuration + XCTAssertEqual(parsed.routingPolicies, [.IPv4, .IPv6]) + } + func testConnectionBlock() throws { - let lines = base + ["", ""] + let lines = ["", ""] XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: lines)) }