From a3822678cfa02d631969b9070d255543abd9a828 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Sat, 8 Oct 2022 08:54:55 +0200 Subject: [PATCH] Support --route-nopull (#280) * Parse --route-no-pull When provided, pull everything except: - Routes - DNS - Proxy Implement with higher granularity compared to OpenVPN. * Apply no-pull mask in tunnel settings Pull server settings by default to match standard OpenVPN behavior. Library was prioritizing client over server. * Add link in CHANGELOG --- CHANGELOG.md | 8 +++ .../OpenVPNTunnelProvider.swift | 71 ++++++++++++------- .../TunnelKitOpenVPNCore/Configuration.swift | 37 +++++++++- .../ConfigurationParser.swift | 9 +++ 4 files changed, 99 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ae710f..3f35ae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- OpenVPN: Support for `--route-nopull`. [#280](https://github.com/passepartoutvpn/tunnelkit/pull/280) + ### Changed - Upgrade OpenSSL to 1.1.1q. +### Fixed + +- OpenVPN: Prioritize server configuration over client (standard behavior). + ## 5.0.0 (2022-09-23) ### Added diff --git a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift index f47d3bb..2046c60 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/OpenVPNTunnelProvider.swift @@ -563,7 +563,12 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } private func bringNetworkUp(remoteAddress: String, localOptions: OpenVPN.Configuration, options: OpenVPN.Configuration, completionHandler: @escaping (Error?) -> Void) { - let routingPolicies = localOptions.routingPolicies ?? options.routingPolicies + let pullMask = localOptions.pullMask + let pullRoutes = pullMask?.contains(.routes) ?? false + let pullDNS = pullMask?.contains(.dns) ?? false + let pullProxy = pullMask?.contains(.proxy) ?? false + + let routingPolicies = pullRoutes ? options.routingPolicies : localOptions.routingPolicies let isIPv4Gateway = routingPolicies?.contains(.IPv4) ?? false let isIPv6Gateway = routingPolicies?.contains(.IPv6) ?? false let isGateway = isIPv4Gateway || isIPv6Gateway @@ -585,13 +590,15 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { log.info("Routing.IPv4: Setting default gateway to \(ipv4.defaultGateway.maskedDescription)") } - for r in ipv4.routes { - let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) - ipv4Route.gatewayAddress = r.gateway - routes.append(ipv4Route) - log.info("Routing.IPv4: Adding route \(r.destination.maskedDescription)/\(r.mask) -> \(r.gateway)") + if pullRoutes { + for r in ipv4.routes { + let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) + ipv4Route.gatewayAddress = r.gateway + routes.append(ipv4Route) + log.info("Routing.IPv4: Adding route \(r.destination.maskedDescription)/\(r.mask) -> \(r.gateway)") + } } - + ipv4Settings = NEIPv4Settings(addresses: [ipv4.address], subnetMasks: [ipv4.addressMask]) ipv4Settings?.includedRoutes = routes ipv4Settings?.excludedRoutes = [] @@ -614,11 +621,13 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { log.info("Routing.IPv6: Setting default gateway to \(ipv6.defaultGateway.maskedDescription)") } - for r in ipv6.routes { - let ipv6Route = NEIPv6Route(destinationAddress: r.destination, networkPrefixLength: r.prefixLength as NSNumber) - ipv6Route.gatewayAddress = r.gateway - routes.append(ipv6Route) - log.info("Routing.IPv6: Adding route \(r.destination.maskedDescription)/\(r.prefixLength) -> \(r.gateway)") + if pullRoutes { + for r in ipv6.routes { + let ipv6Route = NEIPv6Route(destinationAddress: r.destination, networkPrefixLength: r.prefixLength as NSNumber) + ipv6Route.gatewayAddress = r.gateway + routes.append(ipv6Route) + log.info("Routing.IPv6: Adding route \(r.destination.maskedDescription)/\(r.prefixLength) -> \(r.gateway)") + } } ipv6Settings = NEIPv6Settings(addresses: [ipv6.address], networkPrefixLengths: [ipv6.addressPrefixLength as NSNumber]) @@ -671,15 +680,29 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } } + // ensure that non-nil arrays also imply non-empty + if let array = options.dnsServers { + precondition(!array.isEmpty) + } + if let array = options.searchDomains { + precondition(!array.isEmpty) + } + if let array = options.proxyBypassDomains { + precondition(!array.isEmpty) + } + if let array = cfg.configuration.dnsServers { + precondition(!array.isEmpty) + } + if let array = cfg.configuration.searchDomains { + precondition(!array.isEmpty) + } + if let array = cfg.configuration.proxyBypassDomains { + precondition(!array.isEmpty) + } + // fall back if dnsSettings == nil { - dnsServers = [] - if let servers = localOptions.dnsServers, - !servers.isEmpty { - dnsServers = servers - } else if let servers = options.dnsServers { - dnsServers = servers - } + dnsServers = (pullDNS ? options.dnsServers : localOptions.dnsServers) ?? [] if !dnsServers.isEmpty { log.info("DNS: Using servers \(dnsServers.maskedDescription)") dnsSettings = NEDNSSettings(servers: dnsServers) @@ -695,7 +718,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { dnsSettings?.matchDomains = [""] } - if let searchDomains = localOptions.searchDomains ?? options.searchDomains { + if let searchDomains = pullDNS ? options.searchDomains : localOptions.searchDomains { log.info("DNS: Using search domains \(searchDomains.maskedDescription)") dnsSettings?.domainName = searchDomains.first dnsSettings?.searchDomains = searchDomains @@ -718,13 +741,13 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { var proxySettings: NEProxySettings? if localOptions.isProxyEnabled ?? true { - if let httpsProxy = cfg.configuration.httpsProxy ?? options.httpsProxy { + if let httpsProxy = pullProxy ? options.httpsProxy : localOptions.httpsProxy { proxySettings = NEProxySettings() proxySettings?.httpsServer = httpsProxy.neProxy() proxySettings?.httpsEnabled = true log.info("Routing: Setting HTTPS proxy \(httpsProxy.address.maskedDescription):\(httpsProxy.port)") } - if let httpProxy = localOptions.httpProxy ?? options.httpProxy { + if let httpProxy = pullProxy ? options.httpProxy : localOptions.httpProxy { if proxySettings == nil { proxySettings = NEProxySettings() } @@ -732,7 +755,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { proxySettings?.httpEnabled = true log.info("Routing: Setting HTTP proxy \(httpProxy.address.maskedDescription):\(httpProxy.port)") } - if let pacURL = localOptions.proxyAutoConfigurationURL ?? options.proxyAutoConfigurationURL { + if let pacURL = pullProxy ? options.proxyAutoConfigurationURL : localOptions.proxyAutoConfigurationURL { if proxySettings == nil { proxySettings = NEProxySettings() } @@ -742,7 +765,7 @@ extension OpenVPNTunnelProvider: OpenVPNSessionDelegate { } // only set if there is a proxy (proxySettings set to non-nil above) - if let bypass = localOptions.proxyBypassDomains ?? options.proxyBypassDomains { + if let bypass = pullProxy ? options.proxyBypassDomains : localOptions.proxyBypassDomains { proxySettings?.exceptionList = bypass log.info("Routing: Setting proxy by-pass list: \(bypass.maskedDescription)") } diff --git a/Sources/TunnelKitOpenVPNCore/Configuration.swift b/Sources/TunnelKitOpenVPNCore/Configuration.swift index e25a520..bb9a6a1 100644 --- a/Sources/TunnelKitOpenVPNCore/Configuration.swift +++ b/Sources/TunnelKitOpenVPNCore/Configuration.swift @@ -152,6 +152,19 @@ extension OpenVPN { case blockLocal } + /// Settings that can be pulled from server. + public enum PullMask: String, Codable, CaseIterable { + + /// Routes and gateways. + case routes + + /// DNS settings. + case dns + + /// Proxy settings. + case proxy + } + /// The way to create a `Configuration` object for a `OpenVPNSession`. public struct ConfigurationBuilder { @@ -289,6 +302,9 @@ extension OpenVPN { /// Policies for redirecting traffic through the VPN gateway. public var routingPolicies: [RoutingPolicy]? + /// Server settings that must not be pulled. + public var noPullMask: [PullMask]? + /** Creates a `ConfigurationBuilder`. @@ -347,7 +363,8 @@ extension OpenVPN { httpsProxy: httpsProxy, proxyAutoConfigurationURL: proxyAutoConfigurationURL, proxyBypassDomains: proxyBypassDomains, - routingPolicies: routingPolicies + routingPolicies: routingPolicies, + noPullMask: noPullMask ) } } @@ -478,6 +495,9 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.routingPolicies` public let routingPolicies: [RoutingPolicy]? + /// - Seealso: `ConfigurationBuilder.noPullMask` + public let noPullMask: [PullMask]? + // MARK: Shortcuts public var fallbackCipher: Cipher { @@ -495,6 +515,15 @@ extension OpenVPN { public var fallbackCompressionAlgorithm: CompressionAlgorithm { return compressionAlgorithm ?? Fallback.compressionAlgorithm } + + public var pullMask: [PullMask]? { + let all = PullMask.allCases + guard let notPulled = noPullMask else { + return all + } + let pulled = Array(Set(all).subtracting(notPulled)) + return !pulled.isEmpty ? pulled : nil + } } } @@ -523,6 +552,7 @@ extension OpenVPN.Configuration { builder.keepAliveInterval = keepAliveInterval builder.keepAliveTimeout = keepAliveTimeout builder.renegotiatesAfter = renegotiatesAfter + builder.xorMask = xorMask builder.remotes = remotes builder.checksEKU = checksEKU builder.checksSANHost = checksSANHost @@ -547,7 +577,7 @@ extension OpenVPN.Configuration { builder.proxyAutoConfigurationURL = proxyAutoConfigurationURL builder.proxyBypassDomains = proxyBypassDomains builder.routingPolicies = routingPolicies - builder.xorMask = xorMask + builder.noPullMask = noPullMask return builder } } @@ -655,5 +685,8 @@ extension OpenVPN.Configuration { } else { log.info("\tMTU: default") } + if let noPullMask = noPullMask { + log.info("\tNot pulled: \(noPullMask.map(\.rawValue))") + } } } diff --git a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift index 04f5581..6116189 100644 --- a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift +++ b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift @@ -119,6 +119,8 @@ extension OpenVPN { static let redirectGateway = NSRegularExpression("^redirect-gateway.*") + static let routeNoPull = NSRegularExpression("^route-nopull") + // MARK: Unsupported // static let fragment = NSRegularExpression("^fragment +\\d+") @@ -291,6 +293,7 @@ extension OpenVPN { var optProxyAutoConfigurationURL: URL? var optProxyBypass: [String]? var optRedirectGateway: Set? + var optRouteNoPull: Bool? log.verbose("Configuration file:") for line in lines { @@ -702,6 +705,9 @@ extension OpenVPN { optRedirectGateway?.insert(opt) } } + Regex.routeNoPull.enumerateComponents(in: line) { _ in + optRouteNoPull = true + } // @@ -888,6 +894,9 @@ extension OpenVPN { sessionBuilder.httpsProxy = optHTTPSProxy sessionBuilder.proxyAutoConfigurationURL = optProxyAutoConfigurationURL sessionBuilder.proxyBypassDomains = optProxyBypass + if optRouteNoPull ?? false { + sessionBuilder.noPullMask = [.routes, .dns, .proxy] + } if let flags = optRedirectGateway { var policies: Set = []