From a2250686b65bda049d86bc2e71cf22a28e55ab3a Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Thu, 4 Apr 2019 12:40:28 +0200 Subject: [PATCH] Merge OptionsBundle into Configuration FIXME: issues with non-optional .cipher and .compressionFraming Because: - No pushed cipher (nil) is NOT .aes128cbc - No pushed framing (nil) is NOT .disabled Breaks conditions on pushed cipher/framing via PUSH_REPLY. --- .jazzy.yaml | 1 - TunnelKit.xcodeproj/project.pbxproj | 12 - .../AppExtension/TunnelKitProvider.swift | 12 +- .../Sources/Core/ConfigurationParser.swift | 605 +++++++++++++- TunnelKit/Sources/Core/OptionsBundle.swift | 787 ------------------ .../Core/SessionProxy+Configuration.swift | 98 +++ .../Core/SessionProxy+SessionReply.swift | 11 +- TunnelKit/Sources/Core/SessionProxy.swift | 27 +- TunnelKitTests/ConfigurationParserTests.swift | 31 + TunnelKitTests/OptionsBundleTests.swift | 66 -- TunnelKitTests/PushTests.swift | 6 +- 11 files changed, 733 insertions(+), 923 deletions(-) delete mode 100644 TunnelKit/Sources/Core/OptionsBundle.swift delete mode 100644 TunnelKitTests/OptionsBundleTests.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index 5cbb21e..1ac6d2e 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -22,7 +22,6 @@ custom_categories: - StaticKey - SessionProxy - SessionProxyDelegate - - OptionsBundle - OptionsError - SessionReply - IPv4Settings diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index 7468d55..19bcf43 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -137,12 +137,8 @@ 0EC1BBA620D712DE007C4C7B /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */; }; 0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; 0EC1BBA920D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; - 0ECC60D5225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; }; - 0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; }; 0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; }; 0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; }; - 0ECC60DB2254C8190020BEAC /* OptionsBundleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */; }; - 0ECC60DC2254C8190020BEAC /* OptionsBundleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */; }; 0ECE3528212EB7770040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; }; 0ECE352A212EB88E0040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; }; 0ECEB1152252C8E900E9E551 /* tunnelbear.enc.8.ovpn in Resources */ = {isa = PBXBuildFile; fileRef = 0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */; }; @@ -348,9 +344,7 @@ 0EBBF2FF2085196000E36B40 /* NWTCPConnectionState+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWTCPConnectionState+Description.swift"; sourceTree = ""; }; 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = ""; }; 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = ""; }; - 0ECC60D4225497400020BEAC /* OptionsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundle.swift; sourceTree = ""; }; 0ECC60D72254981A0020BEAC /* OptionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsError.swift; sourceTree = ""; }; - 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundleTests.swift; sourceTree = ""; }; 0ECE3527212EB7770040F253 /* CryptoContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoContainer.swift; sourceTree = ""; }; 0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.ovpn; sourceTree = ""; }; 0ECEB1142252C8E900E9E551 /* tunnelbear.enc.8.key */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.key; sourceTree = ""; }; @@ -473,7 +467,6 @@ 0EB2B45E20F0C098004233D7 /* EncryptionPerformanceTests.swift */, 0EB2B45220F0BB44004233D7 /* EncryptionTests.swift */, 0EB2B45820F0BD9A004233D7 /* LinkTests.swift */, - 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */, 0E12B2A22145341B00B4BAE9 /* PacketTests.swift */, 0E245D682135972800B012A2 /* PushTests.swift */, 0EB2B45620F0BD16004233D7 /* RandomTests.swift */, @@ -656,7 +649,6 @@ 0EFEB42D2006D3C800F81029 /* MSS.h */, 0EFEB43D2006D3C800F81029 /* MSS.m */, 0E12B29D21449ADB00B4BAE9 /* NSRegularExpression+Shortcuts.swift */, - 0ECC60D4225497400020BEAC /* OptionsBundle.swift */, 0ECC60D72254981A0020BEAC /* OptionsError.swift */, 0EFEB43E2006D3C800F81029 /* Packet.swift */, 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */, @@ -1155,7 +1147,6 @@ files = ( 0EB2B45720F0BD16004233D7 /* RandomTests.swift in Sources */, 0E011F812196E23700BA59EE /* ConfigurationParserTests.swift in Sources */, - 0ECC60DB2254C8190020BEAC /* OptionsBundleTests.swift in Sources */, 0EB2B45920F0BD9A004233D7 /* LinkTests.swift in Sources */, 0EB2B45520F0BB53004233D7 /* DataManipulationTests.swift in Sources */, 0E50D57521634E0A00FC87A8 /* ControlChannelTests.swift in Sources */, @@ -1246,7 +1237,6 @@ 0EFEB4762006D3C800F81029 /* DataPath.m in Sources */, 0E0C2127212ED29D008AB282 /* SessionProxy+Configuration.swift in Sources */, 0EFEB4692006D3C800F81029 /* Packet.swift in Sources */, - 0ECC60D5225497400020BEAC /* OptionsBundle.swift in Sources */, 0E011F7A2196D93600BA59EE /* SocketType.swift in Sources */, 0EFEB45A2006D3C800F81029 /* TunnelInterface.swift in Sources */, ); @@ -1315,7 +1305,6 @@ 0EFEB49D2006D7F300F81029 /* IOInterface.swift in Sources */, 0E0C2128212ED29D008AB282 /* SessionProxy+Configuration.swift in Sources */, 0EFEB4972006D7F300F81029 /* SessionProxy+Authenticator.swift in Sources */, - 0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */, 0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */, 0EFEB49B2006D7F300F81029 /* Packet.swift in Sources */, ); @@ -1327,7 +1316,6 @@ files = ( 0EA82A3A2190B2B9007960EB /* RandomTests.swift in Sources */, 0E011F822196E23800BA59EE /* ConfigurationParserTests.swift in Sources */, - 0ECC60DC2254C8190020BEAC /* OptionsBundleTests.swift in Sources */, 0EA82A332190B2B9007960EB /* DataPathPerformanceTests.swift in Sources */, 0EA82A372190B2B9007960EB /* LinkTests.swift in Sources */, 0EA82A352190B2B9007960EB /* EncryptionPerformanceTests.swift in Sources */, diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index f4631c0..b20ed6d 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -465,7 +465,11 @@ extension TunnelKitProvider: SessionProxyDelegate { log.info("\tRemote: \(remoteAddress.maskedDescription)") log.info("\tIPv4: \(reply.options.ipv4?.description ?? "not configured")") log.info("\tIPv6: \(reply.options.ipv6?.description ?? "not configured")") - log.info("\tDNS: \(reply.options.dnsServers.map { $0.maskedDescription })") + if let dnsServers = reply.options.dnsServers { + log.info("\tDNS: \(dnsServers.map { $0.maskedDescription })") + } else { + log.info("\tDNS: not configured)") + } log.info("\tDomain: \(reply.options.searchDomain?.maskedDescription ?? "not configured")") bringNetworkUp(remoteAddress: remoteAddress, reply: reply) { (error) in @@ -510,7 +514,7 @@ extension TunnelKitProvider: SessionProxyDelegate { var routes: [NEIPv4Route] = [defaultRoute] for r in ipv4.routes { let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) - ipv4Route.gatewayAddress = r.gateway ?? ipv4.defaultGateway + ipv4Route.gatewayAddress = r.gateway routes.append(ipv4Route) } @@ -527,7 +531,7 @@ extension TunnelKitProvider: SessionProxyDelegate { var routes: [NEIPv6Route] = [defaultRoute] for r in ipv6.routes { let ipv6Route = NEIPv6Route(destinationAddress: r.destination, networkPrefixLength: r.prefixLength as NSNumber) - ipv6Route.gatewayAddress = r.gateway ?? ipv6.defaultGateway + ipv6Route.gatewayAddress = r.gateway routes.append(ipv6Route) } @@ -538,7 +542,7 @@ extension TunnelKitProvider: SessionProxyDelegate { let dnsServers = cfg.sessionConfiguration.dnsServers ?? reply.options.dnsServers let searchDomain = cfg.sessionConfiguration.searchDomain ?? reply.options.searchDomain - let dnsSettings = NEDNSSettings(servers: dnsServers) + let dnsSettings = NEDNSSettings(servers: dnsServers ?? []) dnsSettings.domainName = searchDomain if let searchDomain = searchDomain { dnsSettings.searchDomains = [searchDomain] diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index 9ef1ca6..cc02acc 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -32,6 +32,86 @@ private let log = SwiftyBeaver.self /// Provides methods to parse a `SessionProxy.Configuration` from an .ovpn configuration file. public class ConfigurationParser { + // XXX: parsing is very optimistic + + struct Regex { + + // MARK: General + + static let cipher = NSRegularExpression("^cipher +[^,\\s]+") + + static let auth = NSRegularExpression("^auth +[\\w\\-]+") + + static let compLZO = NSRegularExpression("^comp-lzo.*") + + static let compress = NSRegularExpression("^compress.*") + + static let keyDirection = NSRegularExpression("^key-direction +\\d") + + static let ping = NSRegularExpression("^ping +\\d+") + + static let renegSec = NSRegularExpression("^reneg-sec +\\d+") + + static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") + + static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") + + // MARK: Client + + static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)") + + static let port = NSRegularExpression("^port +\\d+") + + static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?") + + static let eku = NSRegularExpression("^remote-cert-tls +server") + + static let remoteRandom = NSRegularExpression("^remote-random") + + // MARK: Server + + static let authToken = NSRegularExpression("^auth-token +[a-zA-Z0-9/=+]+") + + static let peerId = NSRegularExpression("^peer-id +[0-9]+") + + // MARK: Routing + + static let topology = NSRegularExpression("^topology +(net30|p2p|subnet)") + + static let ifconfig = NSRegularExpression("^ifconfig +[\\d\\.]+ [\\d\\.]+") + + static let ifconfig6 = NSRegularExpression("^ifconfig-ipv6 +[\\da-fA-F:]+/\\d+ [\\da-fA-F:]+") + + static let route = NSRegularExpression("^route +[\\d\\.]+( +[\\d\\.]+){0,2}") + + static let route6 = NSRegularExpression("^route-ipv6 +[\\da-fA-F:]+/\\d+( +[\\da-fA-F:]+){0,2}") + + static let gateway = NSRegularExpression("^route-gateway +[\\d\\.]+") + + static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") + + static let domain = NSRegularExpression("^dhcp-option +DOMAIN +[^ ]+") + + // MARK: Unsupported + +// static let fragment = NSRegularExpression("^fragment +\\d+") + static let fragment = NSRegularExpression("^fragment") + + static let proxy = NSRegularExpression("^\\w+-proxy") + + static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ") + + static let connection = NSRegularExpression("^") + } + + private enum Topology: String { + case net30 + + case p2p + + case subnet + } + /// Result of the parser. public struct ParsingResult { @@ -66,7 +146,7 @@ public class ConfigurationParser { } /** - Parses an .ovpn file as an array of lines. + Parses a configuration from an array of lines. - Parameter lines: The array of lines holding the configuration. - Parameter passphrase: The optional passphrase for encrypted data. @@ -76,58 +156,507 @@ public class ConfigurationParser { - Throws: `OptionsError` if the configuration file is wrong or incomplete. */ public static func parsed(fromLines lines: [String], passphrase: String? = nil, originalURL: URL? = nil, returnsStripped: Bool = false) throws -> ParsingResult { - let options = try OptionsBundle(from: lines, returnsStripped: returnsStripped) - - guard let ca = options.ca else { - throw OptionsError.missingConfiguration(option: "ca") - } - guard let hostname = options.hostname, !options.remotes.isEmpty else { - throw OptionsError.missingConfiguration(option: "remote") - } - let endpointProtocols = options.remotes.map { EndpointProtocol($0.2, $0.1) } + var optStrippedLines: [String]? = returnsStripped ? [] : nil + var optWarning: OptionsError? + var unsupportedError: OptionsError? + var currentBlockName: String? + var currentBlock: [String] = [] + var optCipher: SessionProxy.Cipher? + var optDigest: SessionProxy.Digest? + var optCompressionFraming: SessionProxy.CompressionFraming? + var optCompressionAlgorithm: SessionProxy.CompressionAlgorithm? + var optCA: CryptoContainer? + var optClientCertificate: CryptoContainer? var optClientKey: CryptoContainer? - if let clientKey = options.clientKey, clientKey.isEncrypted { + var optKeyDirection: StaticKey.Direction? + var optTLSKeyLines: [Substring]? + var optTLSStrategy: SessionProxy.TLSWrap.Strategy? + var optKeepAliveSeconds: TimeInterval? + var optRenegotiateAfterSeconds: TimeInterval? + // + var optHostname: String? + var optDefaultProto: SocketType? + var optDefaultPort: UInt16? + var optRemotes: [(String, UInt16?, SocketType?)] = [] // address, port, socket + var optChecksEKU: Bool? + var optRandomizeEndpoint: Bool? + // + var optAuthToken: String? + var optPeerId: UInt32? + // + var optTopology: String? + var optIfconfig4Arguments: [String]? + var optIfconfig6Arguments: [String]? + var optGateway4Arguments: [String]? + var optRoutes4: [(String, String, String?)] = [] // address, netmask, gateway + var optRoutes6: [(String, UInt8, String?)] = [] // destination, prefix, gateway + var optDNSServers: [String] = [] + var optSearchDomain: String? + + log.verbose("Configuration file:") + for line in lines { + log.verbose(line) + + var isHandled = false + var strippedLine = line + defer { + if isHandled { + optStrippedLines?.append(strippedLine) + } + } + + // MARK: Unsupported + + // check blocks first + Regex.connection.enumerateComponents(in: line) { (_) in + unsupportedError = OptionsError.unsupportedConfiguration(option: " blocks") + } + Regex.fragment.enumerateComponents(in: line) { (_) in + unsupportedError = OptionsError.unsupportedConfiguration(option: "fragment") + } + Regex.proxy.enumerateComponents(in: line) { (_) in + unsupportedError = OptionsError.unsupportedConfiguration(option: "proxy: \"\(line)\"") + } + Regex.externalFiles.enumerateComponents(in: line) { (_) in + unsupportedError = OptionsError.unsupportedConfiguration(option: "external file: \"\(line)\"") + } + if line.contains("mtu") || line.contains("mssfix") { + isHandled = true + } + + // MARK: Inline content + + if unsupportedError == nil { + if currentBlockName == nil { + Regex.blockBegin.enumerateComponents(in: line) { + isHandled = true + let tag = $0.first! + let from = tag.index(after: tag.startIndex) + let to = tag.index(before: tag.endIndex) + + currentBlockName = String(tag[from.."] + if $0.count > 1 { + port = UInt16($0[1]) + strippedComponents.append($0[1]) + } + if $0.count > 2 { + proto = SocketType(protoString: $0[2]) + strippedComponents.append($0[2]) + } + optRemotes.append((hostname, port, proto)) + + // replace private data + strippedLine = strippedComponents.joined(separator: " ") + } + Regex.eku.enumerateComponents(in: line) { (_) in + isHandled = true + optChecksEKU = true + } + Regex.remoteRandom.enumerateComponents(in: line) { (_) in + isHandled = true + optRandomizeEndpoint = true + } + + // MARK: Server + + Regex.authToken.enumerateArguments(in: line) { + optAuthToken = $0[0] + } + Regex.peerId.enumerateArguments(in: line) { + optPeerId = UInt32($0[0]) + } + + // MARK: Routing + + Regex.topology.enumerateArguments(in: line) { + optTopology = $0.first + } + Regex.ifconfig.enumerateArguments(in: line) { + optIfconfig4Arguments = $0 + } + Regex.ifconfig6.enumerateArguments(in: line) { + optIfconfig6Arguments = $0 + } + Regex.route.enumerateArguments(in: line) { + let routeEntryArguments = $0 + + let address = routeEntryArguments[0] + let mask = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : "255.255.255.255" + let gateway = (routeEntryArguments.count > 2) ? routeEntryArguments[2] : nil // defaultGateway4 + optRoutes4.append((address, mask, gateway)) + } + Regex.route6.enumerateArguments(in: line) { + let routeEntryArguments = $0 + + let destinationComponents = routeEntryArguments[0].components(separatedBy: "/") + guard destinationComponents.count == 2 else { + return + } + guard let prefix = UInt8(destinationComponents[1]) else { + return + } + + let destination = destinationComponents[0] + let gateway = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : nil // defaultGateway6 + optRoutes6.append((destination, prefix, gateway)) + } + Regex.gateway.enumerateArguments(in: line) { + optGateway4Arguments = $0 + } + Regex.dns.enumerateArguments(in: line) { + guard $0.count == 2 else { + return + } + optDNSServers.append($0[1]) + } + Regex.domain.enumerateArguments(in: line) { + guard $0.count == 2 else { + return + } + optSearchDomain = $0[1] + } + + // + + if let error = unsupportedError { + throw error + } + } + + // + + var sessionBuilder = SessionProxy.ConfigurationBuilder() + + // MARK: General + + sessionBuilder.cipher = optCipher ?? .aes128cbc // FIXME: non-optional breaks PUSH_REPLY + sessionBuilder.digest = optDigest ?? .sha1 // FIXME: non-optional breaks PUSH_REPLY + sessionBuilder.compressionFraming = optCompressionFraming ?? .disabled // FIXME: non-optional breaks PUSH_REPLY + sessionBuilder.compressionAlgorithm = optCompressionAlgorithm ?? .disabled + sessionBuilder.ca = optCA + sessionBuilder.clientCertificate = optClientCertificate + + if let clientKey = optClientKey, clientKey.isEncrypted { guard let passphrase = passphrase else { throw OptionsError.encryptionPassphrase } do { - optClientKey = try clientKey.decrypted(with: passphrase) + sessionBuilder.clientKey = try clientKey.decrypted(with: passphrase) } catch let e { throw OptionsError.unableToDecrypt(error: e) } } else { - optClientKey = options.clientKey + sessionBuilder.clientKey = optClientKey } - var sessionBuilder = SessionProxy.ConfigurationBuilder() - sessionBuilder.ca = ca - sessionBuilder.cipher = options.cipher ?? .aes128cbc - sessionBuilder.digest = options.digest ?? .sha1 - sessionBuilder.compressionFraming = options.compressionFraming ?? .disabled - sessionBuilder.compressionAlgorithm = options.compressionAlgorithm ?? .disabled - sessionBuilder.tlsWrap = options.tlsWrap - sessionBuilder.clientCertificate = options.clientCertificate - sessionBuilder.clientKey = optClientKey - sessionBuilder.hostname = hostname - sessionBuilder.endpointProtocols = endpointProtocols - sessionBuilder.checksEKU = options.checksEKU - sessionBuilder.keepAliveInterval = options.keepAliveSeconds - sessionBuilder.renegotiatesAfter = options.renegotiateAfterSeconds - sessionBuilder.dnsServers = options.dnsServers - sessionBuilder.searchDomain = options.searchDomain - sessionBuilder.randomizeEndpoint = options.randomizeEndpoint + if let keyLines = optTLSKeyLines, let strategy = optTLSStrategy { + let optKey: StaticKey? + switch strategy { + case .auth: + optKey = StaticKey(lines: keyLines, direction: optKeyDirection) + + case .crypt: + optKey = StaticKey(lines: keyLines, direction: .client) + } + if let key = optKey { + sessionBuilder.tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key) + } + } + + sessionBuilder.keepAliveInterval = optKeepAliveSeconds + sessionBuilder.renegotiatesAfter = optRenegotiateAfterSeconds + + // MARK: Client + + optDefaultProto = optDefaultProto ?? .udp + optDefaultPort = optDefaultPort ?? 1194 + if !optRemotes.isEmpty { + sessionBuilder.hostname = optRemotes[0].0 + + var fullRemotes: [(String, UInt16, SocketType)] = [] + let hostname = optRemotes[0].0 + optRemotes.forEach { + guard $0.0 == hostname else { + return + } + guard let port = $0.1 ?? optDefaultPort else { + return + } + guard let socketType = $0.2 ?? optDefaultProto else { + return + } + fullRemotes.append((hostname, port, socketType)) + } + sessionBuilder.endpointProtocols = fullRemotes.map { EndpointProtocol($0.2, $0.1) } + } else { + sessionBuilder.hostname = nil + } + + sessionBuilder.checksEKU = optChecksEKU + sessionBuilder.randomizeEndpoint = optRandomizeEndpoint + + // MARK: Server + + sessionBuilder.authToken = optAuthToken + sessionBuilder.peerId = optPeerId + + // MARK: Routing + + // + // 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". + // + if let ifconfig4Arguments = optIfconfig4Arguments { + guard ifconfig4Arguments.count == 2 else { + throw OptionsError.malformed(option: "ifconfig takes 2 arguments") + } + + let address4: String + let addressMask4: String + let defaultGateway4: String + + let topology = Topology(rawValue: optTopology ?? "") ?? .net30 + switch topology { + case .subnet: + + // default gateway required when topology is subnet + guard let gateway4Arguments = optGateway4Arguments, gateway4Arguments.count == 1 else { + throw OptionsError.malformed(option: "route-gateway takes 1 argument") + } + address4 = ifconfig4Arguments[0] + addressMask4 = ifconfig4Arguments[1] + defaultGateway4 = gateway4Arguments[0] + + default: + address4 = ifconfig4Arguments[0] + addressMask4 = "255.255.255.255" + defaultGateway4 = ifconfig4Arguments[1] + } + let routes4 = optRoutes4.map { IPv4Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway4) } + sessionBuilder.ipv4 = IPv4Settings( + address: address4, + addressMask: addressMask4, + defaultGateway: defaultGateway4, + routes: routes4 + ) + } + + if let ifconfig6Arguments = optIfconfig6Arguments { + guard ifconfig6Arguments.count == 2 else { + throw OptionsError.malformed(option: "ifconfig-ipv6 takes 2 arguments") + } + let address6Components = ifconfig6Arguments[0].components(separatedBy: "/") + guard address6Components.count == 2 else { + throw OptionsError.malformed(option: "ifconfig-ipv6 address must have a /prefix") + } + guard let addressPrefix6 = UInt8(address6Components[1]) else { + throw OptionsError.malformed(option: "ifconfig-ipv6 address prefix must be a 8-bit number") + } + + let address6 = address6Components[0] + let defaultGateway6 = ifconfig6Arguments[1] + let routes6 = optRoutes6.map { IPv6Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway6) } + + sessionBuilder.ipv6 = IPv6Settings( + address: address6, + addressPrefixLength: addressPrefix6, + defaultGateway: defaultGateway6, + routes: routes6 + ) + } + + sessionBuilder.dnsServers = optDNSServers + sessionBuilder.searchDomain = optSearchDomain + + // + return ParsingResult( url: originalURL, configuration: sessionBuilder.build(), - strippedLines: options.strippedLines, - warning: options.warning + strippedLines: optStrippedLines, + warning: optWarning ) } + + private static func normalizeEncryptedPEMBlock(block: inout [String]) { +// if block.count >= 1 && block[0].contains("ENCRYPTED") { +// return true +// } + + // XXX: restore blank line after encryption header (easier than tweaking trimmedLines) + if block.count >= 3 && block[1].contains("Proc-Type") { + block.insert("", at: 3) +// return true + } +// return false + } } -extension String { +private extension String { func trimmedLines() -> [String] { return components(separatedBy: .newlines).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) @@ -136,3 +665,13 @@ extension String { } } } + +private extension SocketType { + init?(protoString: String) { + var str = protoString + if str.hasSuffix("6") { + str.removeLast() + } + self.init(rawValue: str.uppercased()) + } +} diff --git a/TunnelKit/Sources/Core/OptionsBundle.swift b/TunnelKit/Sources/Core/OptionsBundle.swift deleted file mode 100644 index 8332bb8..0000000 --- a/TunnelKit/Sources/Core/OptionsBundle.swift +++ /dev/null @@ -1,787 +0,0 @@ -// -// OptionsBundle.swift -// TunnelKit -// -// Created by Davide De Rosa on 4/3/19. -// Copyright (c) 2019 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 Foundation -import SwiftyBeaver -import __TunnelKitNative - -private let log = SwiftyBeaver.self - -/// Wraps together all recognized options from either configuration files or PUSH_REPLY. -public struct OptionsBundle { - struct Regex { - - // MARK: General - - static let cipher = NSRegularExpression("^cipher +[^,\\s]+") - - static let auth = NSRegularExpression("^auth +[\\w\\-]+") - - static let compLZO = NSRegularExpression("^comp-lzo.*") - - static let compress = NSRegularExpression("^compress.*") - - static let keyDirection = NSRegularExpression("^key-direction +\\d") - - static let ping = NSRegularExpression("^ping +\\d+") - - static let renegSec = NSRegularExpression("^reneg-sec +\\d+") - - static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") - - static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") - - // MARK: Client - - static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)") - - static let port = NSRegularExpression("^port +\\d+") - - static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?") - - static let eku = NSRegularExpression("^remote-cert-tls +server") - - static let remoteRandom = NSRegularExpression("^remote-random") - - // MARK: Server - - static let authToken = NSRegularExpression("^auth-token +[a-zA-Z0-9/=+]+") - - static let peerId = NSRegularExpression("^peer-id +[0-9]+") - - // MARK: Routing - - static let topology = NSRegularExpression("^topology +(net30|p2p|subnet)") - - static let ifconfig = NSRegularExpression("^ifconfig +[\\d\\.]+ [\\d\\.]+") - - static let ifconfig6 = NSRegularExpression("^ifconfig-ipv6 +[\\da-fA-F:]+/\\d+ [\\da-fA-F:]+") - - static let route = NSRegularExpression("^route +[\\d\\.]+( +[\\d\\.]+){0,2}") - - static let route6 = NSRegularExpression("^route-ipv6 +[\\da-fA-F:]+/\\d+( +[\\da-fA-F:]+){0,2}") - - static let gateway = NSRegularExpression("^route-gateway +[\\d\\.]+") - - static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") - - static let domain = NSRegularExpression("^dhcp-option +DOMAIN +[^ ]+") - - // MARK: Unsupported - -// static let fragment = NSRegularExpression("^fragment +\\d+") - static let fragment = NSRegularExpression("^fragment") - - static let proxy = NSRegularExpression("^\\w+-proxy") - - static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ") - - static let connection = NSRegularExpression("^") - } - - private enum Topology: String { - case net30 - - case p2p - - case subnet - } - - public let strippedLines: [String]? - - public let warning: OptionsError? - - // MARK: General - - /// The cipher algorithm for data encryption. - public let cipher: SessionProxy.Cipher? - - /// The digest algorithm for HMAC. - public let digest: SessionProxy.Digest? - - /// Compression framing, disabled by default. - public let compressionFraming: SessionProxy.CompressionFraming? - - /// Compression algorithm, disabled by default. - public let compressionAlgorithm: SessionProxy.CompressionAlgorithm? - - /// The CA for TLS negotiation (PEM format). - public let ca: CryptoContainer? - - /// The optional client certificate for TLS negotiation (PEM format). - public let clientCertificate: CryptoContainer? - - /// The private key for the certificate in `clientCertificate` (PEM format). - public let clientKey: CryptoContainer? - - /// The optional TLS wrapping. - public let tlsWrap: SessionProxy.TLSWrap? - - /// Sends periodical keep-alive packets if set. - public let keepAliveSeconds: TimeInterval? - - /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation. - public let renegotiateAfterSeconds: TimeInterval? - - // MARK: Client - - /// The server hostname (picked from first remote). - public let hostname: String? - - /// The list of server endpoints (address, port, socket). - public let remotes: [(String, UInt16, SocketType)] - - /// If true, checks EKU of server certificate. - public let checksEKU: Bool - - /// Picks endpoint from `remotes` randomly. - public let randomizeEndpoint: Bool - - // MARK: Server - - /// The auth-token returned by the server. - public let authToken: String? - - /// The peer-id returned by the server. - public let peerId: UInt32? - - // MARK: Routing - - /// The settings for IPv4. - public let ipv4: IPv4Settings? - - /// The settings for IPv6. - public let ipv6: IPv6Settings? - - /// The DNS servers. - public let dnsServers: [String] - - /// The search domain. - public let searchDomain: String? - - /** - Parses options from an array of lines. - - - Parameter lines: The array of lines holding the options. - - Parameter returnsStripped: When `true`, stores the stripped lines into `strippedLines`. Defaults to `false`. - - Throws: `OptionsError` if the options are wrong or incomplete. - */ - public init(from lines: [String], returnsStripped: Bool = false) throws { - var optStrippedLines: [String]? = returnsStripped ? [] : nil - var optWarning: OptionsError? - var unsupportedError: OptionsError? - var currentBlockName: String? - var currentBlock: [String] = [] - - var optCipher: SessionProxy.Cipher? - var optDigest: SessionProxy.Digest? - var optCompressionFraming: SessionProxy.CompressionFraming? - var optCompressionAlgorithm: SessionProxy.CompressionAlgorithm? - var optCA: CryptoContainer? - var optClientCertificate: CryptoContainer? - var optClientKey: CryptoContainer? - var optKeyDirection: StaticKey.Direction? - var optTLSKeyLines: [Substring]? - var optTLSStrategy: SessionProxy.TLSWrap.Strategy? - var optKeepAliveSeconds: TimeInterval? - var optRenegotiateAfterSeconds: TimeInterval? - // - var optHostname: String? - var optDefaultProto: SocketType? - var optDefaultPort: UInt16? - var optRemotes: [(String, UInt16?, SocketType?)] = [] // address, port, socket - var optChecksEKU: Bool? - var optRandomizeEndpoint: Bool? - // - var optAuthToken: String? - var optPeerId: UInt32? - // - var optTopology: String? - var optIfconfig4Arguments: [String]? - var optIfconfig6Arguments: [String]? - var optGateway4Arguments: [String]? - var optRoutes4: [(String, String, String?)] = [] // address, netmask, gateway - var optRoutes6: [(String, UInt8, String?)] = [] // destination, prefix, gateway - var optDNSServers: [String] = [] - var optSearchDomain: String? - - log.verbose("Configuration file:") - for line in lines { - log.verbose(line) - - var isHandled = false - var strippedLine = line - defer { - if isHandled { - optStrippedLines?.append(strippedLine) - } - } - - // MARK: Unsupported - - // check blocks first - Regex.connection.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: " blocks") - } - Regex.fragment.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "fragment") - } - Regex.proxy.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "proxy: \"\(line)\"") - } - Regex.externalFiles.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "external file: \"\(line)\"") - } - if line.contains("mtu") || line.contains("mssfix") { - isHandled = true - } - - // MARK: Inline content - - if unsupportedError == nil { - if currentBlockName == nil { - Regex.blockBegin.enumerateComponents(in: line) { - isHandled = true - let tag = $0.first! - let from = tag.index(after: tag.startIndex) - let to = tag.index(before: tag.endIndex) - - currentBlockName = String(tag[from.."] - if $0.count > 1 { - port = UInt16($0[1]) - strippedComponents.append($0[1]) - } - if $0.count > 2 { - proto = SocketType(protoString: $0[2]) - strippedComponents.append($0[2]) - } - optRemotes.append((hostname, port, proto)) - - // replace private data - strippedLine = strippedComponents.joined(separator: " ") - } - Regex.eku.enumerateComponents(in: line) { (_) in - isHandled = true - optChecksEKU = true - } - Regex.remoteRandom.enumerateComponents(in: line) { (_) in - isHandled = true - optRandomizeEndpoint = true - } - - // MARK: Server - - Regex.authToken.enumerateArguments(in: line) { - optAuthToken = $0[0] - } - Regex.peerId.enumerateArguments(in: line) { - optPeerId = UInt32($0[0]) - } - - // MARK: Routing - - Regex.topology.enumerateArguments(in: line) { - optTopology = $0.first - } - Regex.ifconfig.enumerateArguments(in: line) { - optIfconfig4Arguments = $0 - } - Regex.ifconfig6.enumerateArguments(in: line) { - optIfconfig6Arguments = $0 - } - Regex.route.enumerateArguments(in: line) { - let routeEntryArguments = $0 - - let address = routeEntryArguments[0] - let mask = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : "255.255.255.255" - let gateway = (routeEntryArguments.count > 2) ? routeEntryArguments[2] : nil // defaultGateway4 - optRoutes4.append((address, mask, gateway)) - } - Regex.route6.enumerateArguments(in: line) { - let routeEntryArguments = $0 - - let destinationComponents = routeEntryArguments[0].components(separatedBy: "/") - guard destinationComponents.count == 2 else { - return - } - guard let prefix = UInt8(destinationComponents[1]) else { - return - } - - let destination = destinationComponents[0] - let gateway = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : nil // defaultGateway6 - optRoutes6.append((destination, prefix, gateway)) - } - Regex.gateway.enumerateArguments(in: line) { - optGateway4Arguments = $0 - } - Regex.dns.enumerateArguments(in: line) { - guard $0.count == 2 else { - return - } - optDNSServers.append($0[1]) - } - Regex.domain.enumerateArguments(in: line) { - guard $0.count == 2 else { - return - } - optSearchDomain = $0[1] - } - - // - - if let error = unsupportedError { - throw error - } - } - - // - - strippedLines = optStrippedLines - warning = optWarning - - // MARK: General - - cipher = optCipher - digest = optDigest - compressionFraming = optCompressionFraming - compressionAlgorithm = optCompressionAlgorithm - ca = optCA - clientCertificate = optClientCertificate - clientKey = optClientKey - - if let keyLines = optTLSKeyLines, let strategy = optTLSStrategy { - let optKey: StaticKey? - switch strategy { - case .auth: - optKey = StaticKey(lines: keyLines, direction: optKeyDirection) - - case .crypt: - optKey = StaticKey(lines: keyLines, direction: .client) - } - if let key = optKey { - tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key) - } else { - tlsWrap = nil - } - } else { - tlsWrap = nil - } - - keepAliveSeconds = optKeepAliveSeconds - renegotiateAfterSeconds = optRenegotiateAfterSeconds - - // MARK: Client - - optDefaultProto = optDefaultProto ?? .udp - optDefaultPort = optDefaultPort ?? 1194 - if !optRemotes.isEmpty { - hostname = optRemotes[0].0 - - var fullRemotes: [(String, UInt16, SocketType)] = [] - let hostname = optRemotes[0].0 - optRemotes.forEach { - guard $0.0 == hostname else { - return - } - guard let port = $0.1 ?? optDefaultPort else { - return - } - guard let socketType = $0.2 ?? optDefaultProto else { - return - } - fullRemotes.append((hostname, port, socketType)) - } - remotes = fullRemotes - } else { - hostname = nil - remotes = [] - } - - checksEKU = optChecksEKU ?? false - randomizeEndpoint = optRandomizeEndpoint ?? false - - // MARK: Server - - authToken = optAuthToken - peerId = optPeerId - - // MARK: Routing - - // - // 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". - // - if let ifconfig4Arguments = optIfconfig4Arguments { - guard ifconfig4Arguments.count == 2 else { - throw OptionsError.malformed(option: "ifconfig takes 2 arguments") - } - - let address4: String - let addressMask4: String - let defaultGateway4: String - - let topology = Topology(rawValue: optTopology ?? "") ?? .net30 - switch topology { - case .subnet: - - // default gateway required when topology is subnet - guard let gateway4Arguments = optGateway4Arguments, gateway4Arguments.count == 1 else { - throw OptionsError.malformed(option: "route-gateway takes 1 argument") - } - address4 = ifconfig4Arguments[0] - addressMask4 = ifconfig4Arguments[1] - defaultGateway4 = gateway4Arguments[0] - - default: - address4 = ifconfig4Arguments[0] - addressMask4 = "255.255.255.255" - defaultGateway4 = ifconfig4Arguments[1] - } - let routes4 = optRoutes4.map { IPv4Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway4) } - - ipv4 = IPv4Settings( - address: address4, - addressMask: addressMask4, - defaultGateway: defaultGateway4, - routes: routes4 - ) - } else { - ipv4 = nil - } - - if let ifconfig6Arguments = optIfconfig6Arguments { - guard ifconfig6Arguments.count == 2 else { - throw OptionsError.malformed(option: "ifconfig-ipv6 takes 2 arguments") - } - let address6Components = ifconfig6Arguments[0].components(separatedBy: "/") - guard address6Components.count == 2 else { - throw OptionsError.malformed(option: "ifconfig-ipv6 address must have a /prefix") - } - guard let addressPrefix6 = UInt8(address6Components[1]) else { - throw OptionsError.malformed(option: "ifconfig-ipv6 address prefix must be a 8-bit number") - } - - let address6 = address6Components[0] - let defaultGateway6 = ifconfig6Arguments[1] - let routes6 = optRoutes6.map { IPv6Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway6) } - - ipv6 = IPv6Settings( - address: address6, - addressPrefixLength: addressPrefix6, - defaultGateway: defaultGateway6, - routes: routes6 - ) - } else { - ipv6 = nil - } - - dnsServers = optDNSServers - searchDomain = optSearchDomain - } - - private static func normalizeEncryptedPEMBlock(block: inout [String]) { -// if block.count >= 1 && block[0].contains("ENCRYPTED") { -// return true -// } - - // XXX: restore blank line after encryption header (easier than tweaking trimmedLines) - if block.count >= 3 && block[1].contains("Proc-Type") { - block.insert("", at: 3) -// return true - } -// return false - } -} - -/// Encapsulates the IPv4 settings for the tunnel. -public struct IPv4Settings: Codable, CustomStringConvertible { - - /// Represents an IPv4 route in the routing table. - public struct Route: Codable, CustomStringConvertible { - - /// 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 - } - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "{\(destination.maskedDescription)/\(mask) \(gateway?.maskedDescription ?? "default")}" - } - } - - /// The address. - let address: String - - /// The address mask. - let addressMask: String - - /// The address of the default gateway. - let defaultGateway: String - - /// The additional routes. - let routes: [Route] - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "addr \(address.maskedDescription) netmask \(addressMask) gw \(defaultGateway.maskedDescription) routes \(routes.map { $0.maskedDescription })" - } -} - -/// Encapsulates the IPv6 settings for the tunnel. -public struct IPv6Settings: Codable, CustomStringConvertible { - - /// Represents an IPv6 route in the routing table. - public struct Route: Codable, CustomStringConvertible { - - /// The destination host or subnet. - public let destination: String - - /// The address prefix length. - public let prefixLength: UInt8 - - /// The address of the gateway (uses default gateway if not set). - public let gateway: String? - - fileprivate init(_ destination: String, _ prefixLength: UInt8?, _ gateway: String?) { - self.destination = destination - self.prefixLength = prefixLength ?? 3 - self.gateway = gateway - } - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "{\(destination.maskedDescription)/\(prefixLength) \(gateway?.maskedDescription ?? "default")}" - } - } - - /// The address. - public let address: String - - /// The address prefix length. - public let addressPrefixLength: UInt8 - - /// The address of the default gateway. - public let defaultGateway: String - - /// The additional routes. - public let routes: [Route] - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "addr \(address.maskedDescription)/\(addressPrefixLength) gw \(defaultGateway.maskedDescription) routes \(routes.map { $0.maskedDescription })" - } -} - -private extension SocketType { - init?(protoString: String) { - var str = protoString - if str.hasSuffix("6") { - str.removeLast() - } - self.init(rawValue: str.uppercased()) - } -} diff --git a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift index b1620fe..457311f 100644 --- a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift +++ b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift @@ -360,3 +360,101 @@ extension SessionProxy { } } } + +/// Encapsulates the IPv4 settings for the tunnel. +public struct IPv4Settings: Codable, CustomStringConvertible { + + /// Represents an IPv4 route in the routing table. + public struct Route: Codable, CustomStringConvertible { + + /// 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 + + init(_ destination: String, _ mask: String?, _ gateway: String) { + self.destination = destination + self.mask = mask ?? "255.255.255.255" + self.gateway = gateway + } + + // MARK: CustomStringConvertible + + /// :nodoc: + public var description: String { + return "{\(destination.maskedDescription)/\(mask) \(gateway.maskedDescription)}" + } + } + + /// The address. + let address: String + + /// The address mask. + let addressMask: String + + /// The address of the default gateway. + let defaultGateway: String + + /// The additional routes. + let routes: [Route] + + // MARK: CustomStringConvertible + + /// :nodoc: + public var description: String { + return "addr \(address.maskedDescription) netmask \(addressMask) gw \(defaultGateway.maskedDescription) routes \(routes.map { $0.maskedDescription })" + } +} + +/// Encapsulates the IPv6 settings for the tunnel. +public struct IPv6Settings: Codable, CustomStringConvertible { + + /// Represents an IPv6 route in the routing table. + public struct Route: Codable, CustomStringConvertible { + + /// The destination host or subnet. + public let destination: String + + /// The address prefix length. + public let prefixLength: UInt8 + + /// The address of the gateway (uses default gateway if not set). + public let gateway: String + + init(_ destination: String, _ prefixLength: UInt8?, _ gateway: String) { + self.destination = destination + self.prefixLength = prefixLength ?? 3 + self.gateway = gateway + } + + // MARK: CustomStringConvertible + + /// :nodoc: + public var description: String { + return "{\(destination.maskedDescription)/\(prefixLength) \(gateway.maskedDescription)}" + } + } + + /// The address. + public let address: String + + /// The address prefix length. + public let addressPrefixLength: UInt8 + + /// The address of the default gateway. + public let defaultGateway: String + + /// The additional routes. + public let routes: [Route] + + // MARK: CustomStringConvertible + + /// :nodoc: + public var description: String { + return "addr \(address.maskedDescription)/\(addressPrefixLength) gw \(defaultGateway.maskedDescription) routes \(routes.map { $0.maskedDescription })" + } +} diff --git a/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift b/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift index 89b4064..2cb1ccb 100644 --- a/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift +++ b/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift @@ -41,19 +41,16 @@ import Foundation public protocol SessionReply { /// The returned options. - var options: OptionsBundle { get } + var options: SessionProxy.Configuration { get } } extension SessionProxy { - - // XXX: parsing is very optimistic - struct PushReply: SessionReply, CustomStringConvertible { private static let prefix = "PUSH_REPLY," private let original: String - let options: OptionsBundle + let options: SessionProxy.Configuration init?(message: String) throws { guard message.hasPrefix(PushReply.prefix) else { @@ -65,14 +62,14 @@ extension SessionProxy { original = String(message[prefixIndex...]) let lines = original.components(separatedBy: ",") - options = try OptionsBundle(from: lines) + options = try ConfigurationParser.parsed(fromLines: lines).configuration } // MARK: CustomStringConvertible var description: String { let stripped = NSMutableString(string: original) - OptionsBundle.Regex.authToken.replaceMatches( + ConfigurationParser.Regex.authToken.replaceMatches( in: stripped, options: [], range: NSMakeRange(0, stripped.length), diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index f0ca3fe..3f8b6a5 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -86,7 +86,7 @@ public class SessionProxy { private var keepAliveInterval: TimeInterval? { let interval: TimeInterval? - if let negInterval = pushReply?.options.keepAliveSeconds, negInterval > 0 { + if let negInterval = pushReply?.options.keepAliveInterval, negInterval > 0 { interval = TimeInterval(negInterval) } else if let cfgInterval = configuration.keepAliveInterval, cfgInterval > 0.0 { interval = cfgInterval @@ -923,7 +923,10 @@ public class SessionProxy { reply = optionalReply log.debug("Received PUSH_REPLY: \"\(reply.maskedDescription)\"") - if let framing = reply.options.compressionFraming, let compression = reply.options.compressionAlgorithm { + // FIXME: non-optional breaks PUSH_REPLY +// if let framing = reply.options.compressionFraming, let compression = reply.options.compressionAlgorithm { + let framing = reply.options.compressionFraming + if framing != .disabled, let compression = reply.options.compressionAlgorithm { switch compression { case .disabled: break @@ -1041,24 +1044,27 @@ public class SessionProxy { } let pushedFraming = pushReply.options.compressionFraming - if let negFraming = pushedFraming { - log.info("\tNegotiated compression framing: \(negFraming)") - } + // FIXME: non-optional breaks PUSH_REPLY +// if let negFraming = pushedFraming { +// log.info("\tNegotiated compression framing: \(negFraming)") +// } let pushedCompression = pushReply.options.compressionAlgorithm if let negCompression = pushedCompression { log.info("\tNegotiated compression algorithm: \(negCompression)") } - if let negPing = pushReply.options.keepAliveSeconds { - log.info("\tNegotiated keep-alive: \(negPing) seconds") - } let pushedCipher = pushReply.options.cipher - if let negCipher = pushedCipher { - log.info("\tNegotiated cipher: \(negCipher.rawValue)") + // FIXME: non-optional breaks PUSH_REPLY +// if let negCipher = pushedCipher { +// log.info("\tNegotiated cipher: \(negCipher.rawValue)") +// } + if let negPing = pushReply.options.keepAliveInterval { + log.info("\tNegotiated keep-alive: \(negPing) seconds") } let bridge: EncryptionBridge do { bridge = try EncryptionBridge( + // FIXME: non-optional breaks PUSH_REPLY pushedCipher ?? configuration.cipher, configuration.digest, auth, @@ -1074,6 +1080,7 @@ public class SessionProxy { encrypter: bridge.encrypter(), decrypter: bridge.decrypter(), peerId: pushReply.options.peerId ?? PacketPeerIdDisabled, + // FIXME: non-optional breaks PUSH_REPLY compressionFraming: (pushedFraming ?? configuration.compressionFraming).native, compressionAlgorithm: (pushedCompression ?? configuration.compressionAlgorithm ?? .disabled).native, maxPackets: link?.packetBufferSize ?? 200, diff --git a/TunnelKitTests/ConfigurationParserTests.swift b/TunnelKitTests/ConfigurationParserTests.swift index b4b6b7e..034b4a0 100644 --- a/TunnelKitTests/ConfigurationParserTests.swift +++ b/TunnelKitTests/ConfigurationParserTests.swift @@ -27,6 +27,8 @@ 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. @@ -37,6 +39,35 @@ class ConfigurationParserTests: XCTestCase { super.tearDown() } + // 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"])) + + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress lzo"])) + } + + func testDHCPOption() throws { + let lines = base + ["dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com"] + 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") + } + + func testConnectionBlock() throws { + let lines = base + ["", ""] + XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: lines)) + } + + // from file + func testPIA() throws { let file = try ConfigurationParser.parsed(fromURL: url(withName: "pia-hungary")) XCTAssertEqual(file.configuration.hostname, "hungary.privateinternetaccess.com") diff --git a/TunnelKitTests/OptionsBundleTests.swift b/TunnelKitTests/OptionsBundleTests.swift deleted file mode 100644 index 66af67d..0000000 --- a/TunnelKitTests/OptionsBundleTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// OptionsBundleTests.swift -// TunnelKitTests -// -// Created by Davide De Rosa on 4/3/19. -// Copyright (c) 2019 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 -import TunnelKit - -class OptionsBundleTests: 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. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testCompression() throws { -// XCTAssertNotNil(try OptionsBundle.parsed(fromLines: base + ["comp-lzo"]).warning) - XCTAssertNil(try OptionsBundle(from: base + ["comp-lzo"]).warning) - XCTAssertNoThrow(try OptionsBundle(from: base + ["comp-lzo no"])) - XCTAssertNoThrow(try OptionsBundle(from: base + ["comp-lzo yes"])) -// XCTAssertThrowsError(try OptionsBundle(from: base + ["comp-lzo yes"])) - - XCTAssertNoThrow(try OptionsBundle(from: base + ["compress"])) - XCTAssertNoThrow(try OptionsBundle(from: base + ["compress lzo"])) - } - - func testDHCPOption() throws { - let lines = base + ["dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com"] - XCTAssertNoThrow(try OptionsBundle(from: lines)) - - let parsed = try! OptionsBundle(from: lines) - XCTAssertEqual(parsed.dnsServers, ["8.8.8.8", "ffff::1"]) - XCTAssertEqual(parsed.searchDomain, "example.com") - } - - func testConnectionBlock() throws { - let lines = base + ["", ""] - XCTAssertThrowsError(try OptionsBundle(from: lines)) - } -} diff --git a/TunnelKitTests/PushTests.swift b/TunnelKitTests/PushTests.swift index d4963ce..3b4fd82 100644 --- a/TunnelKitTests/PushTests.swift +++ b/TunnelKitTests/PushTests.swift @@ -28,11 +28,11 @@ import XCTest private extension SessionReply { func debug() { - print("Compression framing: \(options.compressionFraming?.description ?? "none")") + print("Compression framing: \(options.compressionFraming.description ?? "none")") print("Compression algorithm: \(options.compressionAlgorithm?.description ?? "none")") print("IPv4: \(options.ipv4?.description ?? "none")") print("IPv6: \(options.ipv6?.description ?? "none")") - print("DNS: \(options.dnsServers)") + print("DNS: \(options.dnsServers?.description ?? "none")") } } @@ -153,6 +153,6 @@ class PushTests: XCTestCase { let reply = try! SessionProxy.PushReply(message: msg)! reply.debug() - XCTAssertEqual(reply.options.keepAliveSeconds, 10) + XCTAssertEqual(reply.options.keepAliveInterval, 10) } }