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.
This commit is contained in:
parent
cfe61d5d40
commit
a2250686b6
|
@ -22,7 +22,6 @@ custom_categories:
|
|||
- StaticKey
|
||||
- SessionProxy
|
||||
- SessionProxyDelegate
|
||||
- OptionsBundle
|
||||
- OptionsError
|
||||
- SessionReply
|
||||
- IPv4Settings
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = "<group>"; };
|
||||
0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = "<group>"; };
|
||||
0ECC60D4225497400020BEAC /* OptionsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundle.swift; sourceTree = "<group>"; };
|
||||
0ECC60D72254981A0020BEAC /* OptionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsError.swift; sourceTree = "<group>"; };
|
||||
0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundleTests.swift; sourceTree = "<group>"; };
|
||||
0ECE3527212EB7770040F253 /* CryptoContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoContainer.swift; sourceTree = "<group>"; };
|
||||
0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.ovpn; sourceTree = "<group>"; };
|
||||
0ECEB1142252C8E900E9E551 /* tunnelbear.enc.8.key */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.key; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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("^<connection>")
|
||||
}
|
||||
|
||||
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: "<connection> 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..<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 = 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)
|
||||
}
|
||||
|
||||
// MARK: Client
|
||||
|
||||
Regex.proto.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
optDefaultProto = SocketType(protoString: str)
|
||||
if optDefaultProto == nil {
|
||||
unsupportedError = OptionsError.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
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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("^<connection>")
|
||||
}
|
||||
|
||||
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: "<connection> 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..<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":
|
||||
OptionsBundle.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 = 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)
|
||||
}
|
||||
|
||||
// MARK: Client
|
||||
|
||||
Regex.proto.enumerateArguments(in: line) {
|
||||
isHandled = true
|
||||
guard let str = $0.first else {
|
||||
return
|
||||
}
|
||||
optDefaultProto = SocketType(protoString: str)
|
||||
if optDefaultProto == nil {
|
||||
unsupportedError = OptionsError.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
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -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 })"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -27,6 +27,8 @@ import XCTest
|
|||
import TunnelKit
|
||||
|
||||
class ConfigurationParserTests: XCTestCase {
|
||||
let base: [String] = ["<ca>", "</ca>", "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 + ["<connection>", "</connection>"]
|
||||
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")
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import TunnelKit
|
||||
|
||||
class OptionsBundleTests: XCTestCase {
|
||||
let base: [String] = ["<ca>", "</ca>", "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 + ["<connection>", "</connection>"]
|
||||
XCTAssertThrowsError(try OptionsBundle(from: lines))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue