224a76ac58
FIXME: for now only redirects ALL traffic when the option is found in the configuration file, whatever the arguments. Also drop unnecessary base options in tests as everything was made optional recently.
771 lines
29 KiB
Swift
771 lines
29 KiB
Swift
//
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
//
|
|
|
|
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 +[^ ]+")
|
|
|
|
static let proxy = NSRegularExpression("^dhcp-option +PROXY_(HTTPS?) +[^ ]+ +\\d+")
|
|
|
|
static let proxyBypass = NSRegularExpression("^dhcp-option +PROXY_BYPASS +.+")
|
|
|
|
static let redirectGateway = NSRegularExpression("^redirect-gateway.*")
|
|
|
|
// MARK: Unsupported
|
|
|
|
// static let fragment = NSRegularExpression("^fragment +\\d+")
|
|
static let fragment = NSRegularExpression("^fragment")
|
|
|
|
static let connectionProxy = NSRegularExpression("^\\w+-proxy")
|
|
|
|
static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ")
|
|
|
|
static let connection = NSRegularExpression("^<connection>")
|
|
|
|
// MARK: Continuation
|
|
|
|
static let continuation = NSRegularExpression("^push-continuation [12]")
|
|
}
|
|
|
|
private enum Topology: String {
|
|
case net30
|
|
|
|
case p2p
|
|
|
|
case subnet
|
|
}
|
|
|
|
private enum RedirectGateway: String {
|
|
case local
|
|
|
|
case autolocal
|
|
|
|
case def1
|
|
|
|
case bypassDHCP = "bypass-dhcp"
|
|
|
|
case bypassDNS = "bypass-dns"
|
|
|
|
case blockLocal = "block-local"
|
|
|
|
case ipv4
|
|
|
|
case noIPv4 = "!ipv4"
|
|
|
|
case ipv6
|
|
}
|
|
|
|
/// 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?
|
|
var optHTTPProxy: Proxy?
|
|
var optHTTPSProxy: Proxy?
|
|
var optProxyBypass: [String]?
|
|
var optRedirectGateway: Set<RedirectGateway>?
|
|
|
|
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: "<connection> blocks")
|
|
}
|
|
Regex.fragment.enumerateComponents(in: line) { (_) in
|
|
unsupportedError = ConfigurationError.unsupportedConfiguration(option: "fragment")
|
|
}
|
|
Regex.connectionProxy.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: Continuation
|
|
|
|
var isContinuation = false
|
|
Regex.continuation.enumerateArguments(in: line) {
|
|
isContinuation = ($0.first == "2")
|
|
}
|
|
guard !isContinuation else {
|
|
throw SessionError.continuationPushReply
|
|
}
|
|
|
|
// 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..<to])
|
|
currentBlock = []
|
|
}
|
|
}
|
|
Regex.blockEnd.enumerateComponents(in: line) {
|
|
isHandled = true
|
|
let tag = $0.first!
|
|
let from = tag.index(tag.startIndex, offsetBy: 2)
|
|
let to = tag.index(before: tag.endIndex)
|
|
|
|
let blockName = String(tag[from..<to])
|
|
guard blockName == currentBlockName else {
|
|
return
|
|
}
|
|
|
|
// first is opening tag
|
|
currentBlock.removeFirst()
|
|
switch blockName {
|
|
case "ca":
|
|
optCA = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
|
|
|
case "cert":
|
|
optClientCertificate = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
|
|
|
case "key":
|
|
ConfigurationParser.normalizeEncryptedPEMBlock(block: ¤tBlock)
|
|
optClientKey = CryptoContainer(pem: currentBlock.joined(separator: "\n"))
|
|
|
|
case "tls-auth":
|
|
optTLSKeyLines = currentBlock.map { Substring($0) }
|
|
optTLSStrategy = .auth
|
|
|
|
case "tls-crypt":
|
|
optTLSKeyLines = currentBlock.map { Substring($0) }
|
|
optTLSStrategy = .crypt
|
|
|
|
default:
|
|
break
|
|
}
|
|
currentBlockName = nil
|
|
currentBlock = []
|
|
}
|
|
}
|
|
if let _ = currentBlockName {
|
|
currentBlock.append(line)
|
|
continue
|
|
}
|
|
|
|
// MARK: General
|
|
|
|
Regex.cipher.enumerateArguments(in: line) {
|
|
isHandled = true
|
|
guard let rawValue = $0.first else {
|
|
return
|
|
}
|
|
optCipher = SessionProxy.Cipher(rawValue: rawValue.uppercased())
|
|
if optCipher == nil {
|
|
unsupportedError = ConfigurationError.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 = ConfigurationError.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)
|
|
}
|
|
|
|
// MARK: Client
|
|
|
|
Regex.proto.enumerateArguments(in: line) {
|
|
isHandled = true
|
|
guard let str = $0.first else {
|
|
return
|
|
}
|
|
optDefaultProto = SocketType(protoString: str)
|
|
if optDefaultProto == nil {
|
|
unsupportedError = ConfigurationError.unsupportedConfiguration(option: "proto \(str)")
|
|
}
|
|
}
|
|
Regex.port.enumerateArguments(in: line) {
|
|
isHandled = true
|
|
guard let str = $0.first else {
|
|
return
|
|
}
|
|
optDefaultPort = UInt16(str)
|
|
}
|
|
Regex.remote.enumerateArguments(in: line) {
|
|
isHandled = true
|
|
guard let hostname = $0.first else {
|
|
return
|
|
}
|
|
var port: UInt16?
|
|
var proto: SocketType?
|
|
var strippedComponents = ["remote", "<hostname>"]
|
|
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
|
|
}
|
|
if optDNSServers == nil {
|
|
optDNSServers = []
|
|
}
|
|
optDNSServers?.append($0[1])
|
|
}
|
|
Regex.domain.enumerateArguments(in: line) {
|
|
guard $0.count == 2 else {
|
|
return
|
|
}
|
|
optSearchDomain = $0[1]
|
|
}
|
|
Regex.proxy.enumerateArguments(in: line) {
|
|
guard $0.count == 3, let port = UInt16($0[2]) else {
|
|
return
|
|
}
|
|
switch $0[0] {
|
|
case "PROXY_HTTPS":
|
|
optHTTPSProxy = Proxy($0[1], port)
|
|
|
|
case "PROXY_HTTP":
|
|
optHTTPProxy = Proxy($0[1], port)
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
Regex.proxyBypass.enumerateArguments(in: line) {
|
|
guard !$0.isEmpty else {
|
|
return
|
|
}
|
|
optProxyBypass = $0
|
|
optProxyBypass?.removeFirst()
|
|
}
|
|
Regex.redirectGateway.enumerateArguments(in: line) {
|
|
|
|
// redirect IPv4 by default
|
|
optRedirectGateway = [.ipv4]
|
|
|
|
for arg in $0 {
|
|
guard let opt = RedirectGateway(rawValue: arg) else {
|
|
continue
|
|
}
|
|
if opt == .noIPv4 {
|
|
optRedirectGateway?.remove(.ipv4)
|
|
} else {
|
|
optRedirectGateway?.insert(opt)
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
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
|
|
sessionBuilder.httpProxy = optHTTPProxy
|
|
sessionBuilder.httpsProxy = optHTTPSProxy
|
|
sessionBuilder.proxyBypassDomains = optProxyBypass
|
|
|
|
// FIXME: only redirects all traffic until --redirect-gateway is properly interpreted
|
|
if let _ = optRedirectGateway {
|
|
sessionBuilder.routingPolicies = [.IPv4, .IPv6]
|
|
}
|
|
|
|
//
|
|
|
|
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())
|
|
}
|
|
}
|