diff --git a/CHANGELOG.md b/CHANGELOG.md index 37282f5..abf2247 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 +### Added + +- Basic support for proxy settings (no PAC). [#74](https://github.com/keeshux/tunnelkit/issues/74) + ### Changed - Make `hostname` optional and pick `resolvedAddresses` if nil. diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index 60ea177..ee17063 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -163,6 +163,18 @@ extension TunnelKitProvider { sessionConfigurationBuilder.usesPIAPatches = providerConfiguration[S.usesPIAPatches] as? Bool ?? ConfigurationBuilder.defaults.sessionConfiguration.usesPIAPatches sessionConfigurationBuilder.dnsServers = providerConfiguration[S.dnsServers] as? [String] sessionConfigurationBuilder.searchDomain = providerConfiguration[S.searchDomain] as? String + if let proxyString = providerConfiguration[S.httpProxy] as? String { + guard let proxy = Proxy(rawValue: proxyString) else { + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.httpProxy)] has a badly formed element") + } + sessionConfigurationBuilder.httpProxy = proxy + } + if let proxyString = providerConfiguration[S.httpsProxy] as? String { + guard let proxy = Proxy(rawValue: proxyString) else { + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.httpsProxy)] has a badly formed element") + } + sessionConfigurationBuilder.httpsProxy = proxy + } sessionConfiguration = sessionConfigurationBuilder.build() shouldDebug = providerConfiguration[S.debug] as? Bool ?? ConfigurationBuilder.defaults.shouldDebug @@ -240,6 +252,10 @@ extension TunnelKitProvider { static let searchDomain = "SearchDomain" + static let httpProxy = "HTTPProxy" + + static let httpsProxy = "HTTPSProxy" + // MARK: Debugging static let debug = "Debug" @@ -443,6 +459,12 @@ extension TunnelKitProvider { if let searchDomain = sessionConfiguration.searchDomain { dict[S.searchDomain] = searchDomain } + if let httpProxy = sessionConfiguration.httpProxy { + dict[S.httpProxy] = httpProxy.rawValue + } + if let httpsProxy = sessionConfiguration.httpsProxy { + dict[S.httpsProxy] = httpsProxy.rawValue + } // if let resolvedAddresses = resolvedAddresses { dict[S.resolvedAddresses] = resolvedAddresses @@ -537,6 +559,12 @@ extension TunnelKitProvider { if let searchDomain = sessionConfiguration.searchDomain { log.info("\tSearch domain: \(searchDomain.maskedDescription)") } + if let httpProxy = sessionConfiguration.httpProxy { + log.info("\tHTTP proxy: \(httpProxy.maskedDescription)") + } + if let httpsProxy = sessionConfiguration.httpsProxy { + log.info("\tHTTPS proxy: \(httpsProxy.maskedDescription)") + } log.info("\tMTU: \(mtu)") log.info("\tDebug: \(shouldDebug)") log.info("\tMasks private data: \(masksPrivateData ?? true)") diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index dfd42ff..f81ff3c 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -554,11 +554,23 @@ extension TunnelKitProvider: SessionProxyDelegate { if let searchDomain = searchDomain { dnsSettings.searchDomains = [searchDomain] } + + var proxySettings: NEProxySettings? + if let httpsProxy = cfg.sessionConfiguration.httpsProxy ?? reply.options.httpsProxy { + proxySettings = NEProxySettings() + proxySettings?.httpsServer = httpsProxy.neProxy() + proxySettings?.httpsEnabled = true + } else if let httpProxy = cfg.sessionConfiguration.httpProxy ?? reply.options.httpProxy { + proxySettings = NEProxySettings() + proxySettings?.httpServer = httpProxy.neProxy() + proxySettings?.httpEnabled = true + } let newSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: remoteAddress) newSettings.ipv4Settings = ipv4Settings newSettings.ipv6Settings = ipv6Settings newSettings.dnsSettings = dnsSettings + newSettings.proxySettings = proxySettings setTunnelNetworkSettings(newSettings, completionHandler: completionHandler) } @@ -671,3 +683,9 @@ extension TunnelKitProvider { return error as? ProviderError ?? .linkError } } + +private extension Proxy { + func neProxy() -> NEProxyServer { + return NEProxyServer(address: address, port: Int(port)) + } +} diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index 625be2e..c419408 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -92,12 +92,14 @@ public class ConfigurationParser { static let domain = NSRegularExpression("^dhcp-option +DOMAIN +[^ ]+") + static let proxy = NSRegularExpression("^dhcp-option +PROXY_(HTTPS?|BYPASS) +[^ ]+ +\\d+") + // MARK: Unsupported // static let fragment = NSRegularExpression("^fragment +\\d+") static let fragment = NSRegularExpression("^fragment") - static let proxy = NSRegularExpression("^\\w+-proxy") + static let connectionProxy = NSRegularExpression("^\\w+-proxy") static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ") @@ -193,7 +195,9 @@ public class ConfigurationParser { var optRoutes6: [(String, UInt8, String?)] = [] // destination, prefix, gateway var optDNSServers: [String] = [] var optSearchDomain: String? - + var optHTTPProxy: Proxy? + var optHTTPSProxy: Proxy? + log.verbose("Configuration file:") for line in lines { log.verbose(line) @@ -215,7 +219,7 @@ public class ConfigurationParser { Regex.fragment.enumerateComponents(in: line) { (_) in unsupportedError = ConfigurationError.unsupportedConfiguration(option: "fragment") } - Regex.proxy.enumerateComponents(in: line) { (_) in + Regex.connectionProxy.enumerateComponents(in: line) { (_) in unsupportedError = ConfigurationError.unsupportedConfiguration(option: "proxy: \"\(line)\"") } Regex.externalFiles.enumerateComponents(in: line) { (_) in @@ -469,6 +473,21 @@ public class ConfigurationParser { } optSearchDomain = $0[1] } + Regex.proxy.enumerateArguments(in: line) { + guard $0.count == 3, let port = UInt16($0[2]) else { + return + } + switch $0[0] { + case "PROXY_HTTPS": + optHTTPSProxy = Proxy($0[1], port) + + case "PROXY_HTTP": + optHTTPProxy = Proxy($0[1], port) + + default: + break + } + } // @@ -631,6 +650,8 @@ public class ConfigurationParser { sessionBuilder.dnsServers = optDNSServers sessionBuilder.searchDomain = optSearchDomain + sessionBuilder.httpProxy = optHTTPProxy + sessionBuilder.httpsProxy = optHTTPSProxy // diff --git a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift index 34487ec..8047293 100644 --- a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift +++ b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift @@ -215,6 +215,12 @@ extension SessionProxy { /// The search domain. public var searchDomain: String? + /// The HTTP proxy. + public var httpProxy: Proxy? + + /// The HTTPS proxy. + public var httpsProxy: Proxy? + /// :nodoc: public init() { } @@ -246,7 +252,9 @@ extension SessionProxy { ipv4: ipv4, ipv6: ipv6, dnsServers: dnsServers, - searchDomain: searchDomain + searchDomain: searchDomain, + httpProxy: httpProxy, + httpsProxy: httpsProxy ) } @@ -334,6 +342,12 @@ extension SessionProxy { /// - Seealso: `SessionProxy.ConfigurationBuilder.searchDomain` public let searchDomain: String? + /// - Seealso: `SessionProxy.ConfigurationBuilder.httpProxy` + public var httpProxy: Proxy? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.httpsProxy` + public var httpsProxy: Proxy? + // MARK: Shortcuts /// :nodoc: @@ -384,6 +398,8 @@ extension SessionProxy.Configuration { builder.ipv6 = ipv6 builder.dnsServers = dnsServers builder.searchDomain = searchDomain + builder.httpProxy = httpProxy + builder.httpsProxy = httpsProxy return builder } } @@ -486,6 +502,45 @@ public struct IPv6Settings: Codable, CustomStringConvertible { } } +/// Encapsulate a proxy setting. +public struct Proxy: Codable, RawRepresentable, CustomStringConvertible { + + /// The proxy address. + public let address: String + + /// The proxy port. + public let port: UInt16 + + /// :nodoc: + public init(_ address: String, _ port: UInt16) { + self.address = address + self.port = port + } + + // MARK: RawRepresentable + + /// :nodoc: + public var rawValue: String { + return "\(address):\(port)" + } + + /// :nodoc: + public init?(rawValue: String) { + let comps = rawValue.components(separatedBy: ":") + guard comps.count == 2, let port = UInt16(comps[1]) else { + return nil + } + self.init(comps[0], port) + } + + // MARK: CustomStringConvertible + + /// :nodoc: + public var description: String { + return rawValue + } +} + /// :nodoc: extension EndpointProtocol: Codable { public init(from decoder: Decoder) throws { diff --git a/TunnelKitTests/ConfigurationParserTests.swift b/TunnelKitTests/ConfigurationParserTests.swift index 034b4a0..9dfc4a1 100644 --- a/TunnelKitTests/ConfigurationParserTests.swift +++ b/TunnelKitTests/ConfigurationParserTests.swift @@ -53,12 +53,22 @@ class ConfigurationParserTests: XCTestCase { } func testDHCPOption() throws { - let lines = base + ["dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com"] + let lines = base + [ + "dhcp-option DNS 8.8.8.8", + "dhcp-option DNS6 ffff::1", + "dhcp-option DOMAIN example.com", + "dhcp-option PROXY_HTTP 1.2.3.4 8081", + "dhcp-option PROXY_HTTPS 7.8.9.10 8082" + ] XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: lines)) let parsed = try! ConfigurationParser.parsed(fromLines: lines).configuration XCTAssertEqual(parsed.dnsServers, ["8.8.8.8", "ffff::1"]) XCTAssertEqual(parsed.searchDomain, "example.com") + XCTAssertEqual(parsed.httpProxy?.address, "1.2.3.4") + XCTAssertEqual(parsed.httpProxy?.port, 8081) + XCTAssertEqual(parsed.httpsProxy?.address, "7.8.9.10") + XCTAssertEqual(parsed.httpsProxy?.port, 8082) } func testConnectionBlock() throws {