From b9b9c4db60ba52e30059d92c0dcdb13fb9c14f50 Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Wed, 3 Apr 2019 10:06:26 +0200 Subject: [PATCH] Parse basic options in OptionsBundle - Handle isEncrypted inside CryptoContainer - Rename ParsingError to OptionsError Reuse OptionsBundle in ConfigurationParser. --- TunnelKit.xcodeproj/project.pbxproj | 12 + .../Sources/Core/ConfigurationParser.swift | 441 ++--------------- TunnelKit/Sources/Core/CryptoContainer.swift | 7 +- TunnelKit/Sources/Core/OptionsBundle.swift | 464 ++++++++++++++++++ TunnelKit/Sources/Core/OptionsError.swift | 42 ++ 5 files changed, 562 insertions(+), 404 deletions(-) create mode 100644 TunnelKit/Sources/Core/OptionsBundle.swift create mode 100644 TunnelKit/Sources/Core/OptionsError.swift diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index a29f644..daa9c73 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -137,6 +137,10 @@ 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 */; }; 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 */; }; @@ -342,6 +346,8 @@ 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 = ""; }; 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 = ""; }; @@ -646,6 +652,8 @@ 0EFEB42D2006D3C800F81029 /* MSS.h */, 0EFEB43D2006D3C800F81029 /* MSS.m */, 0E12B29D21449ADB00B4BAE9 /* NSRegularExpression+Shortcuts.swift */, + 0ECC60D4225497400020BEAC /* OptionsBundle.swift */, + 0ECC60D72254981A0020BEAC /* OptionsError.swift */, 0EFEB43E2006D3C800F81029 /* Packet.swift */, 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */, 0EE7A79720F6296F00B42E6A /* PacketMacros.m */, @@ -1225,6 +1233,7 @@ 0EFEB4722006D3C800F81029 /* ReplayProtector.m in Sources */, 0EFEB4782006D3C800F81029 /* TunnelKitProvider+Configuration.swift in Sources */, 0E3E0F212108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */, + 0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */, 0EFEB4752006D3C800F81029 /* Errors.m in Sources */, 0E58BF532240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */, 0E12B2A521454F7F00B4BAE9 /* BidirectionalState.swift in Sources */, @@ -1232,6 +1241,7 @@ 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 */, ); @@ -1292,6 +1302,7 @@ 0EFEB4AF2007627700F81029 /* InterfaceObserver.swift in Sources */, 0EFEB4A42006D7F300F81029 /* DataPath.m in Sources */, 0EBBF2E62084FE6F00E36B40 /* GenericSocket.swift in Sources */, + 0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */, 0E3E0F222108A8CC00B371C1 /* SessionProxy+PushReply.swift in Sources */, 0E58BF542240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */, 0E12B2A621454F7F00B4BAE9 /* BidirectionalState.swift in Sources */, @@ -1299,6 +1310,7 @@ 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 */, ); diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index d055717..6cc543b 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -32,22 +32,6 @@ private let log = SwiftyBeaver.self /// Provides methods to parse a `SessionProxy.Configuration` from an .ovpn configuration file. public class ConfigurationParser { - /// Error raised by the parser, with details about the line that triggered it. - public enum ParsingError: Error { - - /// The file misses a required option. - case missingConfiguration(option: String) - - /// The file includes an unsupported option. - case unsupportedConfiguration(option: String) - - /// Passphrase required to decrypt private keys. - case encryptionPassphrase - - /// Encryption passphrase is incorrect or key is corrupt. - case unableToDecrypt(error: Error) - } - /// Result of the parser. public struct ParsingResult { @@ -69,51 +53,8 @@ public class ConfigurationParser { /// - Seealso: `ConfigurationParser.parsed(...)` public let strippedLines: [String]? - /// Holds an optional `ParsingError` that didn't block the parser, but it would be worth taking care of. - public let warning: ParsingError? - } - - private struct Regex { - static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)") - - static let port = NSRegularExpression("^port +\\d+") - - static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?") - - 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 ping = NSRegularExpression("^ping +\\d+") - - static let renegSec = NSRegularExpression("^reneg-sec +\\d+") - - static let keyDirection = NSRegularExpression("^key-direction +\\d") - - static let eku = NSRegularExpression("^remote-cert-tls +server") - - static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") - - static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") - - static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") - - static let remoteRandom = NSRegularExpression("^remote-random") - - // 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("^") + /// Holds an optional `OptionsError` that didn't block the parser, but it would be worth taking care of. + public let warning: OptionsError? } /** @@ -123,7 +64,7 @@ public class ConfigurationParser { - Parameter passphrase: The optional passphrase for encrypted data. - Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`. - Returns: The `ParsingResult` outcome of the parsing. - - Throws: `ParsingError` if the configuration file is wrong or incomplete. + - Throws: `OptionsError` if the configuration file is wrong or incomplete. */ public static func parsed(fromURL url: URL, passphrase: String? = nil, returnsStripped: Bool = false) throws -> ParsingResult { let lines = try String(contentsOf: url).trimmedLines() @@ -138,362 +79,56 @@ public class ConfigurationParser { - Parameter originalURL: The optional original URL of the configuration file. - Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`. - Returns: The `ParsingResult` outcome of the parsing. - - Throws: `ParsingError` if the configuration file is wrong or incomplete. + - 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 { - var strippedLines: [String]? = returnsStripped ? [] : nil - var warning: ParsingError? = nil - - var defaultProto: SocketType? - var defaultPort: UInt16? - var remotes: [(String, UInt16?, SocketType?)] = [] - - var cipher: SessionProxy.Cipher? - var digest: SessionProxy.Digest? - var compressionFraming: SessionProxy.CompressionFraming = .disabled - var compressionAlgorithm: SessionProxy.CompressionAlgorithm = .disabled - var optCA: CryptoContainer? - var clientCertificate: CryptoContainer? - var clientKey: CryptoContainer? - var checksEKU = false - var keepAliveSeconds: TimeInterval? - var renegotiateAfterSeconds: TimeInterval? - var keyDirection: StaticKey.Direction? - var tlsStrategy: SessionProxy.TLSWrap.Strategy? - var tlsKeyLines: [Substring]? - var tlsWrap: SessionProxy.TLSWrap? - var dnsServers: [String]? - var randomizeEndpoint = false - - var currentBlockName: String? - var currentBlock: [String] = [] - var unsupportedError: ParsingError? = nil - - log.verbose("Configuration file:") - for line in lines { - log.verbose(line) - - var isHandled = false - var strippedLine = line - defer { - if isHandled { - strippedLines?.append(strippedLine) - } + 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 optClientKey: CryptoContainer? + if let clientKey = options.clientKey, clientKey.isEncrypted { + guard let passphrase = passphrase else { + throw OptionsError.encryptionPassphrase } - - // check blocks first - Regex.connection.enumerateComponents(in: line) { (_) in - unsupportedError = ParsingError.unsupportedConfiguration(option: " blocks") - } - - 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]) - } - remotes.append((hostname, port, proto)) - - // replace private data - strippedLine = strippedComponents.joined(separator: " ") - } - Regex.cipher.enumerateArguments(in: line) { - isHandled = true - guard let rawValue = $0.first else { - return - } - cipher = SessionProxy.Cipher(rawValue: rawValue.uppercased()) - if cipher == nil { - unsupportedError = ParsingError.unsupportedConfiguration(option: "cipher \(rawValue)") - } - } - Regex.auth.enumerateArguments(in: line) { - isHandled = true - guard let rawValue = $0.first else { - return - } - digest = SessionProxy.Digest(rawValue: rawValue.uppercased()) - if digest == nil { - unsupportedError = ParsingError.unsupportedConfiguration(option: "auth \(rawValue)") - } - } - Regex.compLZO.enumerateArguments(in: line) { - isHandled = true - compressionFraming = .compLZO - - if !LZOIsSupported() { - guard let arg = $0.first else { - warning = warning ?? .unsupportedConfiguration(option: line) - return - } - guard arg == "no" else { - unsupportedError = .unsupportedConfiguration(option: line) - return - } - } else { - let arg = $0.first - compressionAlgorithm = (arg == "no") ? .disabled : .LZO - } - } - Regex.compress.enumerateArguments(in: line) { - isHandled = true - compressionFraming = .compress - - if !LZOIsSupported() { - guard $0.isEmpty else { - unsupportedError = .unsupportedConfiguration(option: line) - return - } - } else { - if let arg = $0.first { - compressionAlgorithm = (arg == "lzo") ? .LZO : .other - } else { - compressionAlgorithm = .disabled - } - } - } - Regex.keyDirection.enumerateArguments(in: line) { - isHandled = true - guard let arg = $0.first, let value = Int(arg) else { - return - } - keyDirection = StaticKey.Direction(rawValue: value) - } - Regex.ping.enumerateArguments(in: line) { - isHandled = true - guard let arg = $0.first else { - return - } - keepAliveSeconds = TimeInterval(arg) - } - Regex.renegSec.enumerateArguments(in: line) { - isHandled = true - guard let arg = $0.first else { - return - } - renegotiateAfterSeconds = TimeInterval(arg) - } - Regex.dns.enumerateArguments(in: line) { - isHandled = true - guard $0.count == 2 else { - return - } - if dnsServers == nil { - dnsServers = [] - } - dnsServers?.append($0[1]) - } - Regex.remoteRandom.enumerateComponents(in: line) { (_) in - randomizeEndpoint = true - } - Regex.fragment.enumerateComponents(in: line) { (_) in - unsupportedError = ParsingError.unsupportedConfiguration(option: "fragment") - } - Regex.proxy.enumerateComponents(in: line) { (_) in - unsupportedError = ParsingError.unsupportedConfiguration(option: "proxy: \"\(line)\"") - } - Regex.externalFiles.enumerateComponents(in: line) { (_) in - unsupportedError = ParsingError.unsupportedConfiguration(option: "external file: \"\(line)\"") - } - if line.contains("mtu") || line.contains("mssfix") { - isHandled = true - } - - if let error = unsupportedError { - throw error + do { + optClientKey = try clientKey.decrypted(with: passphrase) + } catch let e { + throw OptionsError.unableToDecrypt(error: e) } + } else { + optClientKey = options.clientKey } - guard let ca = optCA else { - throw ParsingError.missingConfiguration(option: "ca") - } - - // XXX: only reads first remote -// hostnames = remotes.map { $0.0 } - guard !remotes.isEmpty else { - throw ParsingError.missingConfiguration(option: "remote") - } - let hostname = remotes[0].0 - - defaultProto = defaultProto ?? .udp - defaultPort = defaultPort ?? 1194 - - // XXX: reads endpoints from remotes with matching hostname - var endpointProtocols: [EndpointProtocol] = [] - remotes.forEach { - guard $0.0 == hostname else { - return - } - guard let port = $0.1 ?? defaultPort else { - return - } - guard let socketType = $0.2 ?? defaultProto else { - return - } - endpointProtocols.append(EndpointProtocol(socketType, port)) - } - - assert(!endpointProtocols.isEmpty, "Must define an endpoint protocol") - - if let keyLines = tlsKeyLines, let strategy = tlsStrategy { - let optKey: StaticKey? - switch strategy { - case .auth: - optKey = StaticKey(lines: keyLines, direction: keyDirection) - - case .crypt: - optKey = StaticKey(lines: keyLines, direction: .client) - } - if let key = optKey { - tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key) - } - } - var sessionBuilder = SessionProxy.ConfigurationBuilder(ca: ca) - sessionBuilder.cipher = cipher ?? .aes128cbc - sessionBuilder.digest = digest ?? .sha1 - sessionBuilder.compressionFraming = compressionFraming - sessionBuilder.compressionAlgorithm = compressionAlgorithm - sessionBuilder.tlsWrap = tlsWrap - sessionBuilder.clientCertificate = clientCertificate - sessionBuilder.clientKey = clientKey - sessionBuilder.checksEKU = checksEKU - sessionBuilder.keepAliveInterval = keepAliveSeconds - sessionBuilder.renegotiatesAfter = renegotiateAfterSeconds - sessionBuilder.dnsServers = dnsServers - sessionBuilder.randomizeEndpoint = randomizeEndpoint + 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.checksEKU = options.checksEKU + sessionBuilder.keepAliveInterval = options.keepAliveSeconds + sessionBuilder.renegotiatesAfter = options.renegotiateAfterSeconds + sessionBuilder.dnsServers = options.dnsServers + sessionBuilder.randomizeEndpoint = options.randomizeEndpoint return ParsingResult( url: originalURL, hostname: hostname, protocols: endpointProtocols, configuration: sessionBuilder.build(), - strippedLines: strippedLines, - warning: warning + strippedLines: options.strippedLines, + warning: options.warning ) } - - private static func normalizeEncryptedPEMBlock(block: inout [String]) -> Bool { - 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 - } -} - -private extension SocketType { - init?(protoString: String) { - var str = protoString - if str.hasSuffix("6") { - str.removeLast() - } - self.init(rawValue: str.uppercased()) - } } extension String { diff --git a/TunnelKit/Sources/Core/CryptoContainer.swift b/TunnelKit/Sources/Core/CryptoContainer.swift index 1a0b741..0246286 100644 --- a/TunnelKit/Sources/Core/CryptoContainer.swift +++ b/TunnelKit/Sources/Core/CryptoContainer.swift @@ -82,7 +82,12 @@ extension CryptoContainer: Codable { } } -extension CryptoContainer { +/// :nodoc: +public extension CryptoContainer { + var isEncrypted: Bool { + return pem.contains("ENCRYPTED") + } + func decrypted(with passphrase: String) throws -> CryptoContainer { let decryptedPEM = try TLSBox.decryptedPrivateKey(fromPEM: pem, passphrase: passphrase) return CryptoContainer(pem: decryptedPEM) diff --git a/TunnelKit/Sources/Core/OptionsBundle.swift b/TunnelKit/Sources/Core/OptionsBundle.swift new file mode 100644 index 0000000..ef5b9d5 --- /dev/null +++ b/TunnelKit/Sources/Core/OptionsBundle.swift @@ -0,0 +1,464 @@ +// +// 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 + +public struct OptionsBundle { + private struct Regex { + + // shared + + 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 ping = NSRegularExpression("^ping +\\d+") + + static let renegSec = NSRegularExpression("^reneg-sec +\\d+") + + static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") + + static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") + + static let keyDirection = NSRegularExpression("^key-direction +\\d") + + static let gateway = NSRegularExpression("route-gateway [\\d\\.]+") + + 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 dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") + + static let remoteRandom = NSRegularExpression("^remote-random") + + // 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") + + // server + + 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 authToken = NSRegularExpression("auth-token [a-zA-Z0-9/=+]+") + + static let peerId = NSRegularExpression("peer-id [0-9]+") + + // 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("^") + } + + public let strippedLines: [String]? + + public let warning: OptionsError? + + // + + public let hostname: String? + + public let remotes: [(String, UInt16, SocketType)] + + public let cipher: SessionProxy.Cipher? + + public let digest: SessionProxy.Digest? + + public let compressionFraming: SessionProxy.CompressionFraming? + + public let compressionAlgorithm: SessionProxy.CompressionAlgorithm? + + public let ca: CryptoContainer? + + public let clientCertificate: CryptoContainer? + + public let clientKey: CryptoContainer? + + public let checksEKU: Bool + + public let keepAliveSeconds: TimeInterval? + + public let renegotiateAfterSeconds: TimeInterval? + + public let tlsWrap: SessionProxy.TLSWrap? + + public let dnsServers: [String] + + public let randomizeEndpoint: Bool + + public init(from lines: [String], returnsStripped: Bool = false) throws { + var optStrippedLines: [String]? = returnsStripped ? [] : nil + var optWarning: OptionsError? + var unsupportedError: OptionsError? + + var optHostname: String? + var optDefaultProto: SocketType? + var optDefaultPort: UInt16? + var optRemotes: [(String, UInt16?, SocketType?)] = [] + 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 optChecksEKU: Bool? + var optKeepAliveSeconds: TimeInterval? + var optRenegotiateAfterSeconds: TimeInterval? + var optKeyDirection: StaticKey.Direction? + var optTLSKeyLines: [Substring]? + var optTLSStrategy: SessionProxy.TLSWrap.Strategy? + var optDnsServers: [String] = [] + var optRandomizeEndpoint: Bool? + var currentBlockName: String? + var currentBlock: [String] = [] + + log.verbose("Configuration file:") + for line in lines { + log.verbose(line) + + var isHandled = false + var strippedLine = line + defer { + if isHandled { + optStrippedLines?.append(strippedLine) + } + } + + // check blocks first + Regex.connection.enumerateComponents(in: line) { (_) in + unsupportedError = OptionsError.unsupportedConfiguration(option: " blocks") + } + + 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.cipher.enumerateArguments(in: line) { + isHandled = true + guard let rawValue = $0.first else { + return + } + optCipher = SessionProxy.Cipher(rawValue: rawValue.uppercased()) + if optCipher == nil { + unsupportedError = OptionsError.unsupportedConfiguration(option: "cipher \(rawValue)") + } + } + Regex.auth.enumerateArguments(in: line) { + isHandled = true + guard let rawValue = $0.first else { + return + } + optDigest = SessionProxy.Digest(rawValue: rawValue.uppercased()) + if optDigest == nil { + unsupportedError = OptionsError.unsupportedConfiguration(option: "auth \(rawValue)") + } + } + Regex.compLZO.enumerateArguments(in: line) { + isHandled = true + optCompressionFraming = .compLZO + + if !LZOIsSupported() { + guard let arg = $0.first else { + optWarning = optWarning ?? .unsupportedConfiguration(option: line) + return + } + guard arg == "no" else { + unsupportedError = .unsupportedConfiguration(option: line) + return + } + } else { + let arg = $0.first + optCompressionAlgorithm = (arg == "no") ? .disabled : .LZO + } + } + Regex.compress.enumerateArguments(in: line) { + isHandled = true + optCompressionFraming = .compress + + if !LZOIsSupported() { + guard $0.isEmpty else { + unsupportedError = .unsupportedConfiguration(option: line) + return + } + } else { + if let arg = $0.first { + optCompressionAlgorithm = (arg == "lzo") ? .LZO : .other + } else { + optCompressionAlgorithm = .disabled + } + } + } + Regex.keyDirection.enumerateArguments(in: line) { + isHandled = true + guard let arg = $0.first, let value = Int(arg) else { + return + } + optKeyDirection = StaticKey.Direction(rawValue: value) + } + Regex.ping.enumerateArguments(in: line) { + isHandled = true + guard let arg = $0.first else { + return + } + optKeepAliveSeconds = TimeInterval(arg) + } + Regex.renegSec.enumerateArguments(in: line) { + isHandled = true + guard let arg = $0.first else { + return + } + optRenegotiateAfterSeconds = TimeInterval(arg) + } + Regex.dns.enumerateArguments(in: line) { + isHandled = true + guard $0.count == 2 else { + return + } + optDnsServers.append($0[1]) + } + Regex.remoteRandom.enumerateComponents(in: line) { (_) in + optRandomizeEndpoint = true + } + 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 + } + + if let error = unsupportedError { + throw error + } + } + + // + + strippedLines = optStrippedLines + warning = optWarning + + 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 = [] + } + + cipher = optCipher + digest = optDigest + compressionFraming = optCompressionFraming + compressionAlgorithm = optCompressionAlgorithm + ca = optCA + clientCertificate = optClientCertificate + clientKey = optClientKey + checksEKU = optChecksEKU ?? false + keepAliveSeconds = optKeepAliveSeconds + renegotiateAfterSeconds = optRenegotiateAfterSeconds + dnsServers = optDnsServers + randomizeEndpoint = optRandomizeEndpoint ?? false + + 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 + } + } + + 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 + } +} + +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/OptionsError.swift b/TunnelKit/Sources/Core/OptionsError.swift new file mode 100644 index 0000000..779d2c4 --- /dev/null +++ b/TunnelKit/Sources/Core/OptionsError.swift @@ -0,0 +1,42 @@ +// +// OptionsError.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 + +/// Error raised by the options parser, with details about the line that triggered it. +public enum OptionsError: Error { + + /// The file misses a required option. + case missingConfiguration(option: String) + + /// The file includes an unsupported option. + case unsupportedConfiguration(option: String) + + /// Passphrase required to decrypt private keys. + case encryptionPassphrase + + /// Encryption passphrase is incorrect or key is corrupt. + case unableToDecrypt(error: Error) +}