// // ConfigurationParser.swift // TunnelKit // // Created by Davide De Rosa on 9/5/18. // 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 /// 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 Result { /// Original URL of the configuration file, if parsed from an URL. public let url: URL? /// The overall parsed `SessionProxy.Configuration`. public let configuration: SessionProxy.Configuration /// The lines of the configuration file stripped of any sensitive data. Lines that /// the parser does not recognize are discarded in the first place. /// /// - Seealso: `ConfigurationParser.parsed(...)` public let strippedLines: [String]? /// Holds an optional `ConfigurationError` that didn't block the parser, but it would be worth taking care of. public let warning: ConfigurationError? } /** Parses an .ovpn file from an URL. - Parameter url: The URL of the configuration file. - Parameter passphrase: The optional passphrase for encrypted data. - Parameter returnsStripped: When `true`, stores the stripped file into `Result.strippedLines`. Defaults to `false`. - Returns: The `Result` outcome of the parsing. - Throws: `ConfigurationError` if the configuration file is wrong or incomplete. */ public static func parsed(fromURL url: URL, passphrase: String? = nil, returnsStripped: Bool = false) throws -> Result { let lines = try String(contentsOf: url).trimmedLines() return try parsed(fromLines: lines, passphrase: passphrase, originalURL: url, returnsStripped: returnsStripped) } /** 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. - Parameter originalURL: The optional original URL of the configuration file. - Parameter returnsStripped: When `true`, stores the stripped file into `Result.strippedLines`. Defaults to `false`. - Returns: The `Result` outcome of the parsing. - Throws: `ConfigurationError` 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 -> Result { var optStrippedLines: [String]? = returnsStripped ? [] : nil var optWarning: ConfigurationError? var unsupportedError: ConfigurationError? 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 = ConfigurationError.unsupportedConfiguration(option: " blocks") } Regex.fragment.enumerateComponents(in: line) { (_) in unsupportedError = ConfigurationError.unsupportedConfiguration(option: "fragment") } Regex.proxy.enumerateComponents(in: line) { (_) in unsupportedError = ConfigurationError.unsupportedConfiguration(option: "proxy: \"\(line)\"") } Regex.externalFiles.enumerateComponents(in: line) { (_) in unsupportedError = ConfigurationError.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 sessionBuilder.digest = optDigest sessionBuilder.compressionFraming = optCompressionFraming sessionBuilder.compressionAlgorithm = optCompressionAlgorithm sessionBuilder.ca = optCA sessionBuilder.clientCertificate = optClientCertificate if let clientKey = optClientKey, clientKey.isEncrypted { guard let passphrase = passphrase else { throw ConfigurationError.encryptionPassphrase } do { sessionBuilder.clientKey = try clientKey.decrypted(with: passphrase) } catch let e { throw ConfigurationError.unableToDecrypt(error: e) } } else { sessionBuilder.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 { 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 ConfigurationError.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 ConfigurationError.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 ConfigurationError.malformed(option: "ifconfig-ipv6 takes 2 arguments") } let address6Components = ifconfig6Arguments[0].components(separatedBy: "/") guard address6Components.count == 2 else { throw ConfigurationError.malformed(option: "ifconfig-ipv6 address must have a /prefix") } guard let addressPrefix6 = UInt8(address6Components[1]) else { throw ConfigurationError.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 Result( url: originalURL, configuration: sessionBuilder.build(), 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 } } private extension String { func trimmedLines() -> [String] { return components(separatedBy: .newlines).map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty } } } private extension SocketType { init?(protoString: String) { var str = protoString if str.hasSuffix("6") { str.removeLast() } self.init(rawValue: str.uppercased()) } }