diff --git a/.jazzy.yaml b/.jazzy.yaml index 5cbb21e..2b9e783 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -22,8 +22,7 @@ custom_categories: - StaticKey - SessionProxy - SessionProxyDelegate - - OptionsBundle - - OptionsError + - ConfigurationError - SessionReply - IPv4Settings - IPv6Settings diff --git a/Demo/BasicTunnel-iOS/ViewController.swift b/Demo/BasicTunnel-iOS/ViewController.swift index a48939d..c0c79c4 100644 --- a/Demo/BasicTunnel-iOS/ViewController.swift +++ b/Demo/BasicTunnel-iOS/ViewController.swift @@ -88,15 +88,16 @@ extension ViewController { let port = UInt16(textPort.text!)! let credentials = SessionProxy.Credentials(textUsername.text!, textPassword.text!) - var sessionBuilder = SessionProxy.ConfigurationBuilder(ca: ca) - sessionBuilder.cipher = .aes256gcm + var sessionBuilder = SessionProxy.ConfigurationBuilder() + sessionBuilder.ca = ca + sessionBuilder.cipher = .aes128cbc sessionBuilder.digest = .sha1 sessionBuilder.compressionFraming = .compLZO sessionBuilder.renegotiatesAfter = nil + let socketType: SocketType = switchTCP.isOn ? .tcp : .udp + sessionBuilder.endpointProtocols = [EndpointProtocol(socketType, port)] sessionBuilder.usesPIAPatches = true var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) - let socketType: SocketType = switchTCP.isOn ? .tcp : .udp - builder.endpointProtocols = [EndpointProtocol(socketType, port)] builder.mtu = 1350 builder.shouldDebug = true builder.masksPrivateData = false diff --git a/Demo/BasicTunnel-macOS/ViewController.swift b/Demo/BasicTunnel-macOS/ViewController.swift index 657d84d..4e97d4e 100644 --- a/Demo/BasicTunnel-macOS/ViewController.swift +++ b/Demo/BasicTunnel-macOS/ViewController.swift @@ -88,16 +88,17 @@ extension ViewController { let port = UInt16(textPort.stringValue)! let credentials = SessionProxy.Credentials(textUsername.stringValue, textPassword.stringValue) - var sessionBuilder = SessionProxy.ConfigurationBuilder(ca: ca) + var sessionBuilder = SessionProxy.ConfigurationBuilder() + sessionBuilder.ca = ca sessionBuilder.cipher = .aes128cbc sessionBuilder.digest = .sha1 sessionBuilder.compressionFraming = .compLZO sessionBuilder.renegotiatesAfter = nil - sessionBuilder.usesPIAPatches = true - var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) // let socketType: SocketType = isTCP ? .tcp : .udp let socketType: SocketType = .udp - builder.endpointProtocols = [EndpointProtocol(socketType, port)] + sessionBuilder.endpointProtocols = [EndpointProtocol(socketType, port)] + sessionBuilder.usesPIAPatches = true + var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) builder.mtu = 1350 builder.shouldDebug = true builder.masksPrivateData = false diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index 7468d55..42ba565 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -137,12 +137,8 @@ 0EC1BBA620D712DE007C4C7B /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */; }; 0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; 0EC1BBA920D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; - 0ECC60D5225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; }; - 0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D4225497400020BEAC /* OptionsBundle.swift */; }; - 0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; }; - 0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* OptionsError.swift */; }; - 0ECC60DB2254C8190020BEAC /* OptionsBundleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */; }; - 0ECC60DC2254C8190020BEAC /* OptionsBundleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */; }; + 0ECC60D82254981A0020BEAC /* ConfigurationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* ConfigurationError.swift */; }; + 0ECC60D92254981A0020BEAC /* ConfigurationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECC60D72254981A0020BEAC /* ConfigurationError.swift */; }; 0ECE3528212EB7770040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; }; 0ECE352A212EB88E0040F253 /* CryptoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* CryptoContainer.swift */; }; 0ECEB1152252C8E900E9E551 /* tunnelbear.enc.8.ovpn in Resources */ = {isa = PBXBuildFile; fileRef = 0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */; }; @@ -348,9 +344,7 @@ 0EBBF2FF2085196000E36B40 /* NWTCPConnectionState+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWTCPConnectionState+Description.swift"; sourceTree = ""; }; 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = ""; }; 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = ""; }; - 0ECC60D4225497400020BEAC /* OptionsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundle.swift; sourceTree = ""; }; - 0ECC60D72254981A0020BEAC /* OptionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsError.swift; sourceTree = ""; }; - 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsBundleTests.swift; sourceTree = ""; }; + 0ECC60D72254981A0020BEAC /* ConfigurationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationError.swift; sourceTree = ""; }; 0ECE3527212EB7770040F253 /* CryptoContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoContainer.swift; sourceTree = ""; }; 0ECEB1132252C8E900E9E551 /* tunnelbear.enc.8.ovpn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.ovpn; sourceTree = ""; }; 0ECEB1142252C8E900E9E551 /* tunnelbear.enc.8.key */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tunnelbear.enc.8.key; sourceTree = ""; }; @@ -473,7 +467,6 @@ 0EB2B45E20F0C098004233D7 /* EncryptionPerformanceTests.swift */, 0EB2B45220F0BB44004233D7 /* EncryptionTests.swift */, 0EB2B45820F0BD9A004233D7 /* LinkTests.swift */, - 0ECC60DA2254C8190020BEAC /* OptionsBundleTests.swift */, 0E12B2A22145341B00B4BAE9 /* PacketTests.swift */, 0E245D682135972800B012A2 /* PushTests.swift */, 0EB2B45620F0BD16004233D7 /* RandomTests.swift */, @@ -624,6 +617,7 @@ 0E12B2A421454F7F00B4BAE9 /* BidirectionalState.swift */, 0E58BF4F2240F98E006FB157 /* CompressionAlgorithmNative.h */, 0E245D6B2137F73600B012A2 /* CompressionFramingNative.h */, + 0ECC60D72254981A0020BEAC /* ConfigurationError.swift */, 0E011F872196E2AB00BA59EE /* ConfigurationParser.swift */, 0E39BCE6214B2AB60035E9DE /* ControlPacket.h */, 0E39BCE7214B2AB60035E9DE /* ControlPacket.m */, @@ -656,8 +650,6 @@ 0EFEB42D2006D3C800F81029 /* MSS.h */, 0EFEB43D2006D3C800F81029 /* MSS.m */, 0E12B29D21449ADB00B4BAE9 /* NSRegularExpression+Shortcuts.swift */, - 0ECC60D4225497400020BEAC /* OptionsBundle.swift */, - 0ECC60D72254981A0020BEAC /* OptionsError.swift */, 0EFEB43E2006D3C800F81029 /* Packet.swift */, 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */, 0EE7A79720F6296F00B42E6A /* PacketMacros.m */, @@ -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 */, @@ -1238,7 +1229,7 @@ 0EFEB4722006D3C800F81029 /* ReplayProtector.m in Sources */, 0EFEB4782006D3C800F81029 /* TunnelKitProvider+Configuration.swift in Sources */, 0E3E0F212108A8CC00B371C1 /* SessionProxy+SessionReply.swift in Sources */, - 0ECC60D82254981A0020BEAC /* OptionsError.swift in Sources */, + 0ECC60D82254981A0020BEAC /* ConfigurationError.swift in Sources */, 0EFEB4752006D3C800F81029 /* Errors.m in Sources */, 0E58BF532240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */, 0E12B2A521454F7F00B4BAE9 /* BidirectionalState.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 */, ); @@ -1307,7 +1297,7 @@ 0EFEB4AF2007627700F81029 /* InterfaceObserver.swift in Sources */, 0EFEB4A42006D7F300F81029 /* DataPath.m in Sources */, 0EBBF2E62084FE6F00E36B40 /* GenericSocket.swift in Sources */, - 0ECC60D92254981A0020BEAC /* OptionsError.swift in Sources */, + 0ECC60D92254981A0020BEAC /* ConfigurationError.swift in Sources */, 0E3E0F222108A8CC00B371C1 /* SessionProxy+SessionReply.swift in Sources */, 0E58BF542240FAA6006FB157 /* SessionProxy+CompressionAlgorithm.swift in Sources */, 0E12B2A621454F7F00B4BAE9 /* BidirectionalState.swift in Sources */, @@ -1315,7 +1305,6 @@ 0EFEB49D2006D7F300F81029 /* IOInterface.swift in Sources */, 0E0C2128212ED29D008AB282 /* SessionProxy+Configuration.swift in Sources */, 0EFEB4972006D7F300F81029 /* SessionProxy+Authenticator.swift in Sources */, - 0ECC60D6225497400020BEAC /* OptionsBundle.swift in Sources */, 0E011F7B2196D93600BA59EE /* SocketType.swift in Sources */, 0EFEB49B2006D7F300F81029 /* Packet.swift in Sources */, ); @@ -1327,7 +1316,6 @@ files = ( 0EA82A3A2190B2B9007960EB /* RandomTests.swift in Sources */, 0E011F822196E23800BA59EE /* ConfigurationParserTests.swift in Sources */, - 0ECC60DC2254C8190020BEAC /* OptionsBundleTests.swift in Sources */, 0EA82A332190B2B9007960EB /* DataPathPerformanceTests.swift in Sources */, 0EA82A372190B2B9007960EB /* LinkTests.swift in Sources */, 0EA82A352190B2B9007960EB /* EncryptionPerformanceTests.swift in Sources */, diff --git a/TunnelKit/Sources/AppExtension/ConnectionStrategy.swift b/TunnelKit/Sources/AppExtension/ConnectionStrategy.swift index 3837830..fbe09de 100644 --- a/TunnelKit/Sources/AppExtension/ConnectionStrategy.swift +++ b/TunnelKit/Sources/AppExtension/ConnectionStrategy.swift @@ -59,11 +59,13 @@ class ConnectionStrategy { prefersResolvedAddresses = configuration.prefersResolvedAddresses resolvedAddresses = configuration.resolvedAddresses - if configuration.sessionConfiguration.randomizeEndpoint ?? false { - endpointProtocols = configuration.endpointProtocols.shuffled() - } else { - endpointProtocols = configuration.endpointProtocols + guard var endpointProtocols = configuration.sessionConfiguration.endpointProtocols else { + fatalError("No endpoints defined") } + if configuration.sessionConfiguration.randomizeEndpoint ?? false { + endpointProtocols.shuffle() + } + self.endpointProtocols = endpointProtocols } func createSocket( diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index 6e7c160..3dfd5d3 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -52,25 +52,9 @@ extension TunnelKitProvider { public static let defaults = Configuration( prefersResolvedAddresses: false, resolvedAddresses: nil, - endpointProtocols: [EndpointProtocol(.udp, 1194)], + endpointProtocols: nil, mtu: 1250, - sessionConfiguration: SessionProxy.Configuration( - cipher: .aes128cbc, - digest: .sha1, - ca: CryptoContainer(pem: ""), - clientCertificate: nil, - clientKey: nil, - checksEKU: false, - compressionFraming: .disabled, - compressionAlgorithm: .disabled, - tlsWrap: nil, - keepAliveInterval: nil, - renegotiatesAfter: nil, - dnsServers: nil, - searchDomain: nil, - randomizeEndpoint: false, - usesPIAPatches: nil - ), + sessionConfiguration: SessionProxy.ConfigurationBuilder().build(), shouldDebug: false, debugLogFormat: nil, masksPrivateData: true @@ -84,9 +68,6 @@ extension TunnelKitProvider { /// Resolved addresses in case DNS fails or `prefersResolvedAddresses` is `true`. public var resolvedAddresses: [String]? - /// The accepted communication protocols. Must be non-empty. - public var endpointProtocols: [EndpointProtocol] - /// The MTU of the link. public var mtu: Int @@ -114,7 +95,6 @@ extension TunnelKitProvider { public init(sessionConfiguration: SessionProxy.Configuration) { prefersResolvedAddresses = ConfigurationBuilder.defaults.prefersResolvedAddresses resolvedAddresses = nil - endpointProtocols = ConfigurationBuilder.defaults.endpointProtocols mtu = ConfigurationBuilder.defaults.mtu self.sessionConfiguration = sessionConfiguration shouldDebug = ConfigurationBuilder.defaults.shouldDebug @@ -127,15 +107,6 @@ extension TunnelKitProvider { prefersResolvedAddresses = providerConfiguration[S.prefersResolvedAddresses] as? Bool ?? ConfigurationBuilder.defaults.prefersResolvedAddresses resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String] - guard let endpointProtocolsStrings = providerConfiguration[S.endpointProtocols] as? [String], !endpointProtocolsStrings.isEmpty else { - throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] is nil or empty") - } - endpointProtocols = try endpointProtocolsStrings.map { - guard let ep = EndpointProtocol(rawValue: $0) else { - throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] has a badly formed element") - } - return ep - } mtu = providerConfiguration[S.mtu] as? Int ?? ConfigurationBuilder.defaults.mtu // @@ -166,7 +137,8 @@ extension TunnelKitProvider { clientKey = nil } - var sessionConfigurationBuilder = SessionProxy.ConfigurationBuilder(ca: ca) + var sessionConfigurationBuilder = SessionProxy.ConfigurationBuilder() + sessionConfigurationBuilder.ca = ca sessionConfigurationBuilder.cipher = cipher sessionConfigurationBuilder.digest = digest sessionConfigurationBuilder.clientCertificate = clientCertificate @@ -190,6 +162,15 @@ extension TunnelKitProvider { } sessionConfigurationBuilder.keepAliveInterval = providerConfiguration[S.keepAlive] as? TimeInterval ?? ConfigurationBuilder.defaults.sessionConfiguration.keepAliveInterval sessionConfigurationBuilder.renegotiatesAfter = providerConfiguration[S.renegotiatesAfter] as? TimeInterval ?? ConfigurationBuilder.defaults.sessionConfiguration.renegotiatesAfter + guard let endpointProtocolsStrings = providerConfiguration[S.endpointProtocols] as? [String], !endpointProtocolsStrings.isEmpty else { + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] is nil or empty") + } + sessionConfigurationBuilder.endpointProtocols = try endpointProtocolsStrings.map { + guard let ep = EndpointProtocol(rawValue: $0) else { + throw ProviderConfigurationError.parameter(name: "protocolConfiguration.providerConfiguration[\(S.endpointProtocols)] has a badly formed element") + } + return ep + } sessionConfigurationBuilder.checksEKU = providerConfiguration[S.checksEKU] as? Bool ?? ConfigurationBuilder.defaults.sessionConfiguration.checksEKU sessionConfigurationBuilder.dnsServers = providerConfiguration[S.dnsServers] as? [String] sessionConfigurationBuilder.searchDomain = providerConfiguration[S.searchDomain] as? String @@ -217,7 +198,7 @@ extension TunnelKitProvider { return Configuration( prefersResolvedAddresses: prefersResolvedAddresses, resolvedAddresses: resolvedAddresses, - endpointProtocols: endpointProtocols, + endpointProtocols: nil, mtu: mtu, sessionConfiguration: sessionConfiguration, shouldDebug: shouldDebug, @@ -236,8 +217,6 @@ extension TunnelKitProvider { static let resolvedAddresses = "ResolvedAddresses" - static let endpointProtocols = "EndpointProtocols" - static let mtu = "MTU" // MARK: SessionConfiguration @@ -260,6 +239,8 @@ extension TunnelKitProvider { static let keepAlive = "KeepAlive" + static let endpointProtocols = "EndpointProtocols" + static let renegotiatesAfter = "RenegotiatesAfter" static let checksEKU = "ChecksEKU" @@ -287,8 +268,9 @@ extension TunnelKitProvider { /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.resolvedAddresses` public let resolvedAddresses: [String]? - /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.endpointProtocols` - public let endpointProtocols: [EndpointProtocol] + /// - Seealso: `SessionProxy.Configuration.endpointProtocols` + @available(*, deprecated) + public var endpointProtocols: [EndpointProtocol]? /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.mtu` public let mtu: Int @@ -417,29 +399,39 @@ extension TunnelKitProvider { public func generatedProviderConfiguration(appGroup: String) -> [String: Any] { let S = Keys.self + guard let ca = sessionConfiguration.ca else { + fatalError("No sessionConfiguration.ca set") + } + guard let endpointProtocols = sessionConfiguration.endpointProtocols else { + fatalError("No sessionConfiguration.endpointProtocols set") + } + var dict: [String: Any] = [ S.appGroup: appGroup, S.prefersResolvedAddresses: prefersResolvedAddresses, + S.ca: ca.pem, S.endpointProtocols: endpointProtocols.map { $0.rawValue }, - S.cipherAlgorithm: sessionConfiguration.cipher.rawValue, - S.digestAlgorithm: sessionConfiguration.digest.rawValue, - S.ca: sessionConfiguration.ca.pem, S.mtu: mtu, S.debug: shouldDebug ] + if let cipher = sessionConfiguration.cipher { + dict[S.cipherAlgorithm] = cipher.rawValue + } + if let digest = sessionConfiguration.digest { + dict[S.digestAlgorithm] = digest.rawValue + } + if let compressionFraming = sessionConfiguration.compressionFraming { + dict[S.compressionFraming] = compressionFraming.rawValue + } + if let compressionAlgorithm = sessionConfiguration.compressionAlgorithm { + dict[S.compressionAlgorithm] = compressionAlgorithm.rawValue + } if let clientCertificate = sessionConfiguration.clientCertificate { dict[S.clientCertificate] = clientCertificate.pem } if let clientKey = sessionConfiguration.clientKey { dict[S.clientKey] = clientKey.pem } - if let resolvedAddresses = resolvedAddresses { - dict[S.resolvedAddresses] = resolvedAddresses - } - dict[S.compressionFraming] = sessionConfiguration.compressionFraming.rawValue - if let compressionAlgorithm = sessionConfiguration.compressionAlgorithm?.rawValue { - dict[S.compressionAlgorithm] = compressionAlgorithm - } if let tlsWrapData = sessionConfiguration.tlsWrap?.serialized() { dict[S.tlsWrap] = tlsWrapData } @@ -452,17 +444,21 @@ extension TunnelKitProvider { if let checksEKU = sessionConfiguration.checksEKU { dict[S.checksEKU] = checksEKU } + if let randomizeEndpoint = sessionConfiguration.randomizeEndpoint { + dict[S.randomizeEndpoint] = randomizeEndpoint + } + if let usesPIAPatches = sessionConfiguration.usesPIAPatches { + dict[S.usesPIAPatches] = usesPIAPatches + } if let dnsServers = sessionConfiguration.dnsServers { dict[S.dnsServers] = dnsServers } if let searchDomain = sessionConfiguration.searchDomain { dict[S.searchDomain] = searchDomain } - if let randomizeEndpoint = sessionConfiguration.randomizeEndpoint { - dict[S.randomizeEndpoint] = randomizeEndpoint - } - if let usesPIAPatches = sessionConfiguration.usesPIAPatches { - dict[S.usesPIAPatches] = usesPIAPatches + // + if let resolvedAddresses = resolvedAddresses { + dict[S.resolvedAddresses] = resolvedAddresses } if let debugLogFormat = debugLogFormat { dict[S.debugLogFormat] = debugLogFormat @@ -504,29 +500,32 @@ extension TunnelKitProvider { } func print(appVersion: String?) { + guard let endpointProtocols = sessionConfiguration.endpointProtocols else { + fatalError("No sessionConfiguration.endpointProtocols set") + } + if let appVersion = appVersion { log.info("App version: \(appVersion)") } log.info("\tProtocols: \(endpointProtocols)") - log.info("\tCipher: \(sessionConfiguration.cipher)") - log.info("\tDigest: \(sessionConfiguration.digest)") + log.info("\tCipher: \(sessionConfiguration.fallbackCipher)") + log.info("\tDigest: \(sessionConfiguration.fallbackDigest)") + log.info("\tCompression framing: \(sessionConfiguration.fallbackCompressionFraming)") + if let compressionAlgorithm = sessionConfiguration.compressionAlgorithm, compressionAlgorithm != .disabled { + log.info("\tCompression algorithm: \(compressionAlgorithm)") + } else { + log.info("\tCompression algorithm: disabled") + } if let _ = sessionConfiguration.clientCertificate { log.info("\tClient verification: enabled") } else { log.info("\tClient verification: disabled") } - if sessionConfiguration.checksEKU ?? false { - log.info("\tServer EKU verification: enabled") + if let tlsWrap = sessionConfiguration.tlsWrap { + log.info("\tTLS wrapping: \(tlsWrap.strategy)") } else { - log.info("\tServer EKU verification: disabled") - } - log.info("\tMTU: \(mtu)") - log.info("\tCompression framing: \(sessionConfiguration.compressionFraming)") - if let compressionAlgorithm = sessionConfiguration.compressionAlgorithm, compressionAlgorithm != .disabled { - log.info("\tCompression algorithm: \(compressionAlgorithm)") - } else { - log.info("\tCompression algorithm: disabled") + log.info("\tTLS wrapping: disabled") } if let keepAliveSeconds = sessionConfiguration.keepAliveInterval, keepAliveSeconds > 0 { log.info("\tKeep-alive: \(keepAliveSeconds) seconds") @@ -538,20 +537,21 @@ extension TunnelKitProvider { } else { log.info("\tRenegotiation: never") } - if let tlsWrap = sessionConfiguration.tlsWrap { - log.info("\tTLS wrapping: \(tlsWrap.strategy)") + if sessionConfiguration.checksEKU ?? false { + log.info("\tServer EKU verification: enabled") } else { - log.info("\tTLS wrapping: disabled") - } - if let dnsServers = sessionConfiguration.dnsServers { - log.info("\tCustom DNS servers: \(dnsServers.maskedDescription)") - } - if let searchDomain = sessionConfiguration.searchDomain { - log.info("\tCustom search domain: \(searchDomain.maskedDescription)") + log.info("\tServer EKU verification: disabled") } if sessionConfiguration.randomizeEndpoint ?? false { log.info("\tRandomize endpoint: true") } + if let dnsServers = sessionConfiguration.dnsServers { + log.info("\tDNS servers: \(dnsServers.maskedDescription)") + } + if let searchDomain = sessionConfiguration.searchDomain { + log.info("\tSearch domain: \(searchDomain.maskedDescription)") + } + log.info("\tMTU: \(mtu)") log.info("\tDebug: \(shouldDebug)") log.info("\tMasks private data: \(masksPrivateData ?? true)") } @@ -560,7 +560,7 @@ extension TunnelKitProvider { // MARK: Modification -extension TunnelKitProvider.Configuration: Equatable { +extension TunnelKitProvider.Configuration { /** Returns a `TunnelKitProvider.ConfigurationBuilder` to use this configuration as a starting point for a new one. @@ -569,38 +569,11 @@ extension TunnelKitProvider.Configuration: Equatable { */ public func builder() -> TunnelKitProvider.ConfigurationBuilder { var builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionConfiguration) - builder.endpointProtocols = endpointProtocols builder.mtu = mtu builder.shouldDebug = shouldDebug builder.debugLogFormat = debugLogFormat return builder } - - /// :nodoc: - public static func ==(lhs: TunnelKitProvider.Configuration, rhs: TunnelKitProvider.Configuration) -> Bool { - return ( - (lhs.endpointProtocols == rhs.endpointProtocols) && - (lhs.mtu == rhs.mtu) && - (lhs.sessionConfiguration == rhs.sessionConfiguration) - // XXX: tlsWrap not copied - ) - } -} - -/// :nodoc: -extension EndpointProtocol: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - guard let proto = try EndpointProtocol(rawValue: container.decode(String.self)) else { - throw TunnelKitProvider.ProviderConfigurationError.parameter(name: "endpointProtocol.decodable") - } - self.init(proto.socketType, proto.port) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } } /// :nodoc: diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index f4631c0..b20ed6d 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -465,7 +465,11 @@ extension TunnelKitProvider: SessionProxyDelegate { log.info("\tRemote: \(remoteAddress.maskedDescription)") log.info("\tIPv4: \(reply.options.ipv4?.description ?? "not configured")") log.info("\tIPv6: \(reply.options.ipv6?.description ?? "not configured")") - log.info("\tDNS: \(reply.options.dnsServers.map { $0.maskedDescription })") + if let dnsServers = reply.options.dnsServers { + log.info("\tDNS: \(dnsServers.map { $0.maskedDescription })") + } else { + log.info("\tDNS: not configured)") + } log.info("\tDomain: \(reply.options.searchDomain?.maskedDescription ?? "not configured")") bringNetworkUp(remoteAddress: remoteAddress, reply: reply) { (error) in @@ -510,7 +514,7 @@ extension TunnelKitProvider: SessionProxyDelegate { var routes: [NEIPv4Route] = [defaultRoute] for r in ipv4.routes { let ipv4Route = NEIPv4Route(destinationAddress: r.destination, subnetMask: r.mask) - ipv4Route.gatewayAddress = r.gateway ?? ipv4.defaultGateway + ipv4Route.gatewayAddress = r.gateway routes.append(ipv4Route) } @@ -527,7 +531,7 @@ extension TunnelKitProvider: SessionProxyDelegate { var routes: [NEIPv6Route] = [defaultRoute] for r in ipv6.routes { let ipv6Route = NEIPv6Route(destinationAddress: r.destination, networkPrefixLength: r.prefixLength as NSNumber) - ipv6Route.gatewayAddress = r.gateway ?? ipv6.defaultGateway + ipv6Route.gatewayAddress = r.gateway routes.append(ipv6Route) } @@ -538,7 +542,7 @@ extension TunnelKitProvider: SessionProxyDelegate { let dnsServers = cfg.sessionConfiguration.dnsServers ?? reply.options.dnsServers let searchDomain = cfg.sessionConfiguration.searchDomain ?? reply.options.searchDomain - let dnsSettings = NEDNSSettings(servers: dnsServers) + let dnsSettings = NEDNSSettings(servers: dnsServers ?? []) dnsSettings.domainName = searchDomain if let searchDomain = searchDomain { dnsSettings.searchDomains = [searchDomain] diff --git a/TunnelKit/Sources/Core/OptionsError.swift b/TunnelKit/Sources/Core/ConfigurationError.swift similarity index 89% rename from TunnelKit/Sources/Core/OptionsError.swift rename to TunnelKit/Sources/Core/ConfigurationError.swift index 070b625..37d33b1 100644 --- a/TunnelKit/Sources/Core/OptionsError.swift +++ b/TunnelKit/Sources/Core/ConfigurationError.swift @@ -1,5 +1,5 @@ // -// OptionsError.swift +// ConfigurationError.swift // TunnelKit // // Created by Davide De Rosa on 4/3/19. @@ -25,8 +25,8 @@ import Foundation -/// Error raised by the options parser, with details about the line that triggered it. -public enum OptionsError: Error { +/// Error raised by the configuration parser, with details about the line that triggered it. +public enum ConfigurationError: Error { /// Option syntax is incorrect. case malformed(option: String) diff --git a/TunnelKit/Sources/Core/ConfigurationParser.swift b/TunnelKit/Sources/Core/ConfigurationParser.swift index bee3e85..625be2e 100644 --- a/TunnelKit/Sources/Core/ConfigurationParser.swift +++ b/TunnelKit/Sources/Core/ConfigurationParser.swift @@ -32,26 +32,103 @@ private let log = SwiftyBeaver.self /// Provides methods to parse a `SessionProxy.Configuration` from an .ovpn configuration file. public class ConfigurationParser { + // XXX: parsing is very optimistic + + struct Regex { + + // MARK: General + + static let cipher = NSRegularExpression("^cipher +[^,\\s]+") + + static let auth = NSRegularExpression("^auth +[\\w\\-]+") + + static let compLZO = NSRegularExpression("^comp-lzo.*") + + static let compress = NSRegularExpression("^compress.*") + + static let keyDirection = NSRegularExpression("^key-direction +\\d") + + static let ping = NSRegularExpression("^ping +\\d+") + + static let renegSec = NSRegularExpression("^reneg-sec +\\d+") + + static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") + + static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") + + // MARK: Client + + static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)") + + static let port = NSRegularExpression("^port +\\d+") + + static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?") + + static let eku = NSRegularExpression("^remote-cert-tls +server") + + static let remoteRandom = NSRegularExpression("^remote-random") + + // MARK: Server + + static let authToken = NSRegularExpression("^auth-token +[a-zA-Z0-9/=+]+") + + static let peerId = NSRegularExpression("^peer-id +[0-9]+") + + // MARK: Routing + + static let topology = NSRegularExpression("^topology +(net30|p2p|subnet)") + + static let ifconfig = NSRegularExpression("^ifconfig +[\\d\\.]+ [\\d\\.]+") + + static let ifconfig6 = NSRegularExpression("^ifconfig-ipv6 +[\\da-fA-F:]+/\\d+ [\\da-fA-F:]+") + + static let route = NSRegularExpression("^route +[\\d\\.]+( +[\\d\\.]+){0,2}") + + static let route6 = NSRegularExpression("^route-ipv6 +[\\da-fA-F:]+/\\d+( +[\\da-fA-F:]+){0,2}") + + static let gateway = NSRegularExpression("^route-gateway +[\\d\\.]+") + + static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") + + static let domain = NSRegularExpression("^dhcp-option +DOMAIN +[^ ]+") + + // MARK: Unsupported + +// static let fragment = NSRegularExpression("^fragment +\\d+") + static let fragment = NSRegularExpression("^fragment") + + static let proxy = NSRegularExpression("^\\w+-proxy") + + static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ") + + static let connection = NSRegularExpression("^") + } + + private enum Topology: String { + case net30 + + case p2p + + case subnet + } + /// Result of the parser. - public struct ParsingResult { + public struct Result { /// Original URL of the configuration file, if parsed from an URL. public let url: URL? - /// The main endpoint hostname. - public let hostname: String - - /// The list of `EndpointProtocol` to which the client can connect to. - public let protocols: [EndpointProtocol] - /// The overall parsed `SessionProxy.Configuration`. public let configuration: SessionProxy.Configuration - /// - Seealso: `OptionsBundle.init(...)` + /// 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]? - /// - Seealso: `OptionsBundle.warning` - public let warning: OptionsError? + /// Holds an optional `ConfigurationError` that didn't block the parser, but it would be worth taking care of. + public let warning: ConfigurationError? } /** @@ -59,77 +136,527 @@ public class ConfigurationParser { - 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 `ParsingResult.strippedLines`. Defaults to `false`. - - Returns: The `ParsingResult` outcome of the parsing. - - Throws: `OptionsError` if the configuration file is wrong or incomplete. + - 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 -> ParsingResult { + 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 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. - Parameter originalURL: The optional original URL of the configuration file. - - Parameter returnsStripped: When `true`, stores the stripped file into `ParsingResult.strippedLines`. Defaults to `false`. - - Returns: The `ParsingResult` outcome of the parsing. - - Throws: `OptionsError` if the configuration file is wrong or incomplete. + - 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 -> 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) } + 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? - 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 = ConfigurationError.unsupportedConfiguration(option: " blocks") + } + Regex.fragment.enumerateComponents(in: line) { (_) in + unsupportedError = ConfigurationError.unsupportedConfiguration(option: "fragment") + } + Regex.proxy.enumerateComponents(in: line) { (_) in + unsupportedError = ConfigurationError.unsupportedConfiguration(option: "proxy: \"\(line)\"") + } + Regex.externalFiles.enumerateComponents(in: line) { (_) in + unsupportedError = ConfigurationError.unsupportedConfiguration(option: "external file: \"\(line)\"") + } + if line.contains("mtu") || line.contains("mssfix") { + isHandled = true + } + + // MARK: Inline content + + if unsupportedError == nil { + if currentBlockName == nil { + Regex.blockBegin.enumerateComponents(in: line) { + isHandled = true + let tag = $0.first! + let from = tag.index(after: tag.startIndex) + let to = tag.index(before: tag.endIndex) + + currentBlockName = String(tag[from.."] + if $0.count > 1 { + port = UInt16($0[1]) + strippedComponents.append($0[1]) + } + if $0.count > 2 { + proto = SocketType(protoString: $0[2]) + strippedComponents.append($0[2]) + } + optRemotes.append((hostname, port, proto)) + + // replace private data + strippedLine = strippedComponents.joined(separator: " ") + } + Regex.eku.enumerateComponents(in: line) { (_) in + isHandled = true + optChecksEKU = true + } + Regex.remoteRandom.enumerateComponents(in: line) { (_) in + isHandled = true + optRandomizeEndpoint = true + } + + // MARK: Server + + Regex.authToken.enumerateArguments(in: line) { + optAuthToken = $0[0] + } + Regex.peerId.enumerateArguments(in: line) { + optPeerId = UInt32($0[0]) + } + + // MARK: Routing + + Regex.topology.enumerateArguments(in: line) { + optTopology = $0.first + } + Regex.ifconfig.enumerateArguments(in: line) { + optIfconfig4Arguments = $0 + } + Regex.ifconfig6.enumerateArguments(in: line) { + optIfconfig6Arguments = $0 + } + Regex.route.enumerateArguments(in: line) { + let routeEntryArguments = $0 + + let address = routeEntryArguments[0] + let mask = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : "255.255.255.255" + let gateway = (routeEntryArguments.count > 2) ? routeEntryArguments[2] : nil // defaultGateway4 + optRoutes4.append((address, mask, gateway)) + } + Regex.route6.enumerateArguments(in: line) { + let routeEntryArguments = $0 + + let destinationComponents = routeEntryArguments[0].components(separatedBy: "/") + guard destinationComponents.count == 2 else { + return + } + guard let prefix = UInt8(destinationComponents[1]) else { + return + } + + let destination = destinationComponents[0] + let gateway = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : nil // defaultGateway6 + optRoutes6.append((destination, prefix, gateway)) + } + Regex.gateway.enumerateArguments(in: line) { + optGateway4Arguments = $0 + } + Regex.dns.enumerateArguments(in: line) { + guard $0.count == 2 else { + return + } + optDNSServers.append($0[1]) + } + Regex.domain.enumerateArguments(in: line) { + guard $0.count == 2 else { + return + } + optSearchDomain = $0[1] + } + + // + + if let error = unsupportedError { + throw error + } + } + + // + + var sessionBuilder = SessionProxy.ConfigurationBuilder() + + // MARK: General + + sessionBuilder.cipher = optCipher + sessionBuilder.digest = optDigest + sessionBuilder.compressionFraming = optCompressionFraming + sessionBuilder.compressionAlgorithm = optCompressionAlgorithm + sessionBuilder.ca = optCA + sessionBuilder.clientCertificate = optClientCertificate + + if let clientKey = optClientKey, clientKey.isEncrypted { guard let passphrase = passphrase else { - throw OptionsError.encryptionPassphrase + throw ConfigurationError.encryptionPassphrase } do { - optClientKey = try clientKey.decrypted(with: passphrase) + sessionBuilder.clientKey = try clientKey.decrypted(with: passphrase) } catch let e { - throw OptionsError.unableToDecrypt(error: e) + throw ConfigurationError.unableToDecrypt(error: e) } } else { - optClientKey = options.clientKey + sessionBuilder.clientKey = optClientKey } - var sessionBuilder = SessionProxy.ConfigurationBuilder(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.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 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) } - return ParsingResult( + sessionBuilder.ipv4 = IPv4Settings( + address: address4, + addressMask: addressMask4, + defaultGateway: defaultGateway4, + routes: routes4 + ) + } + + if let ifconfig6Arguments = optIfconfig6Arguments { + guard ifconfig6Arguments.count == 2 else { + throw ConfigurationError.malformed(option: "ifconfig-ipv6 takes 2 arguments") + } + let address6Components = ifconfig6Arguments[0].components(separatedBy: "/") + guard address6Components.count == 2 else { + throw ConfigurationError.malformed(option: "ifconfig-ipv6 address must have a /prefix") + } + guard let addressPrefix6 = UInt8(address6Components[1]) else { + throw ConfigurationError.malformed(option: "ifconfig-ipv6 address prefix must be a 8-bit number") + } + + let address6 = address6Components[0] + let defaultGateway6 = ifconfig6Arguments[1] + let routes6 = optRoutes6.map { IPv6Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway6) } + + sessionBuilder.ipv6 = IPv6Settings( + address: address6, + addressPrefixLength: addressPrefix6, + defaultGateway: defaultGateway6, + routes: routes6 + ) + } + + sessionBuilder.dnsServers = optDNSServers + sessionBuilder.searchDomain = optSearchDomain + + // + + return Result( url: originalURL, - hostname: hostname, - protocols: endpointProtocols, 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) @@ -138,3 +665,13 @@ extension String { } } } + +private extension SocketType { + init?(protoString: String) { + var str = protoString + if str.hasSuffix("6") { + str.removeLast() + } + self.init(rawValue: str.uppercased()) + } +} diff --git a/TunnelKit/Sources/Core/OptionsBundle.swift b/TunnelKit/Sources/Core/OptionsBundle.swift deleted file mode 100644 index c0bb447..0000000 --- a/TunnelKit/Sources/Core/OptionsBundle.swift +++ /dev/null @@ -1,792 +0,0 @@ -// -// OptionsBundle.swift -// TunnelKit -// -// Created by Davide De Rosa on 4/3/19. -// Copyright (c) 2019 Davide De Rosa. All rights reserved. -// -// https://github.com/keeshux -// -// This file is part of TunnelKit. -// -// TunnelKit is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// TunnelKit is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with TunnelKit. If not, see . -// - -import Foundation -import SwiftyBeaver -import __TunnelKitNative - -private let log = SwiftyBeaver.self - -/// Wraps together all recognized options from either configuration files or PUSH_REPLY. -public struct OptionsBundle { - struct Regex { - - // MARK: General - - static let cipher = NSRegularExpression("^cipher +[^,\\s]+") - - static let auth = NSRegularExpression("^auth +[\\w\\-]+") - - static let compLZO = NSRegularExpression("^comp-lzo.*") - - static let compress = NSRegularExpression("^compress.*") - - static let keyDirection = NSRegularExpression("^key-direction +\\d") - - static let ping = NSRegularExpression("^ping +\\d+") - - static let renegSec = NSRegularExpression("^reneg-sec +\\d+") - - static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") - - static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") - - // MARK: Client - - static let proto = NSRegularExpression("^proto +(udp6?|tcp6?)") - - static let port = NSRegularExpression("^port +\\d+") - - static let remote = NSRegularExpression("^remote +[^ ]+( +\\d+)?( +(udp6?|tcp6?))?") - - static let eku = NSRegularExpression("^remote-cert-tls +server") - - static let remoteRandom = NSRegularExpression("^remote-random") - - // MARK: Server - - static let authToken = NSRegularExpression("^auth-token +[a-zA-Z0-9/=+]+") - - static let peerId = NSRegularExpression("^peer-id +[0-9]+") - - // MARK: Routing - - static let topology = NSRegularExpression("^topology +(net30|p2p|subnet)") - - static let ifconfig = NSRegularExpression("^ifconfig +[\\d\\.]+ [\\d\\.]+") - - static let ifconfig6 = NSRegularExpression("^ifconfig-ipv6 +[\\da-fA-F:]+/\\d+ [\\da-fA-F:]+") - - static let route = NSRegularExpression("^route +[\\d\\.]+( +[\\d\\.]+){0,2}") - - static let route6 = NSRegularExpression("^route-ipv6 +[\\da-fA-F:]+/\\d+( +[\\da-fA-F:]+){0,2}") - - static let gateway = NSRegularExpression("^route-gateway +[\\d\\.]+") - - static let dns = NSRegularExpression("^dhcp-option +DNS6? +[\\d\\.a-fA-F:]+") - - static let domain = NSRegularExpression("^dhcp-option +DOMAIN +[^ ]+") - - // MARK: Unsupported - -// static let fragment = NSRegularExpression("^fragment +\\d+") - static let fragment = NSRegularExpression("^fragment") - - static let proxy = NSRegularExpression("^\\w+-proxy") - - static let externalFiles = NSRegularExpression("^(ca|cert|key|tls-auth|tls-crypt) ") - - static let connection = NSRegularExpression("^") - } - - private enum Topology: String { - case net30 - - case p2p - - case subnet - } - - /// 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: `OptionsBundle.init(...)` - public let strippedLines: [String]? - - /// Holds an optional `OptionsError` that didn't block the parser, but it would be worth taking care of. - public let warning: OptionsError? - - // MARK: General - - /// The cipher algorithm for data encryption. - public let cipher: SessionProxy.Cipher? - - /// The digest algorithm for HMAC. - public let digest: SessionProxy.Digest? - - /// Compression framing, disabled by default. - public let compressionFraming: SessionProxy.CompressionFraming? - - /// Compression algorithm, disabled by default. - public let compressionAlgorithm: SessionProxy.CompressionAlgorithm? - - /// The CA for TLS negotiation (PEM format). - public let ca: CryptoContainer? - - /// The optional client certificate for TLS negotiation (PEM format). - public let clientCertificate: CryptoContainer? - - /// The private key for the certificate in `clientCertificate` (PEM format). - public let clientKey: CryptoContainer? - - /// The optional TLS wrapping. - public let tlsWrap: SessionProxy.TLSWrap? - - /// Sends periodical keep-alive packets if set. - public let keepAliveSeconds: TimeInterval? - - /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation. - public let renegotiateAfterSeconds: TimeInterval? - - // MARK: Client - - /// The server hostname (picked from first remote). - public let hostname: String? - - /// The list of server endpoints (address, port, socket). - public let remotes: [(String, UInt16, SocketType)] - - /// If true, checks EKU of server certificate. - public let checksEKU: Bool - - /// Picks endpoint from `remotes` randomly. - public let randomizeEndpoint: Bool - - // MARK: Server - - /// The auth-token returned by the server. - public let authToken: String? - - /// The peer-id returned by the server. - public let peerId: UInt32? - - // MARK: Routing - - /// The settings for IPv4. - public let ipv4: IPv4Settings? - - /// The settings for IPv6. - public let ipv6: IPv6Settings? - - /// The DNS servers. - public let dnsServers: [String] - - /// The search domain. - public let searchDomain: String? - - /** - Parses options from an array of lines. - - - Parameter lines: The array of lines holding the options. - - Parameter returnsStripped: When `true`, stores the stripped lines into `strippedLines`. Defaults to `false`. - - Throws: `OptionsError` if the options are wrong or incomplete. - */ - public init(from lines: [String], returnsStripped: Bool = false) throws { - var optStrippedLines: [String]? = returnsStripped ? [] : nil - var optWarning: OptionsError? - var unsupportedError: OptionsError? - var currentBlockName: String? - var currentBlock: [String] = [] - - var optCipher: SessionProxy.Cipher? - var optDigest: SessionProxy.Digest? - var optCompressionFraming: SessionProxy.CompressionFraming? - var optCompressionAlgorithm: SessionProxy.CompressionAlgorithm? - var optCA: CryptoContainer? - var optClientCertificate: CryptoContainer? - var optClientKey: CryptoContainer? - var optKeyDirection: StaticKey.Direction? - var optTLSKeyLines: [Substring]? - var optTLSStrategy: SessionProxy.TLSWrap.Strategy? - var optKeepAliveSeconds: TimeInterval? - var optRenegotiateAfterSeconds: TimeInterval? - // - var optHostname: String? - var optDefaultProto: SocketType? - var optDefaultPort: UInt16? - var optRemotes: [(String, UInt16?, SocketType?)] = [] // address, port, socket - var optChecksEKU: Bool? - var optRandomizeEndpoint: Bool? - // - var optAuthToken: String? - var optPeerId: UInt32? - // - var optTopology: String? - var optIfconfig4Arguments: [String]? - var optIfconfig6Arguments: [String]? - var optGateway4Arguments: [String]? - var optRoutes4: [(String, String, String?)] = [] // address, netmask, gateway - var optRoutes6: [(String, UInt8, String?)] = [] // destination, prefix, gateway - var optDNSServers: [String] = [] - var optSearchDomain: String? - - log.verbose("Configuration file:") - for line in lines { - log.verbose(line) - - var isHandled = false - var strippedLine = line - defer { - if isHandled { - optStrippedLines?.append(strippedLine) - } - } - - // MARK: Unsupported - - // check blocks first - Regex.connection.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: " blocks") - } - Regex.fragment.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "fragment") - } - Regex.proxy.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "proxy: \"\(line)\"") - } - Regex.externalFiles.enumerateComponents(in: line) { (_) in - unsupportedError = OptionsError.unsupportedConfiguration(option: "external file: \"\(line)\"") - } - if line.contains("mtu") || line.contains("mssfix") { - isHandled = true - } - - // MARK: Inline content - - if unsupportedError == nil { - if currentBlockName == nil { - Regex.blockBegin.enumerateComponents(in: line) { - isHandled = true - let tag = $0.first! - let from = tag.index(after: tag.startIndex) - let to = tag.index(before: tag.endIndex) - - currentBlockName = String(tag[from.."] - if $0.count > 1 { - port = UInt16($0[1]) - strippedComponents.append($0[1]) - } - if $0.count > 2 { - proto = SocketType(protoString: $0[2]) - strippedComponents.append($0[2]) - } - optRemotes.append((hostname, port, proto)) - - // replace private data - strippedLine = strippedComponents.joined(separator: " ") - } - Regex.eku.enumerateComponents(in: line) { (_) in - isHandled = true - optChecksEKU = true - } - Regex.remoteRandom.enumerateComponents(in: line) { (_) in - isHandled = true - optRandomizeEndpoint = true - } - - // MARK: Server - - Regex.authToken.enumerateArguments(in: line) { - optAuthToken = $0[0] - } - Regex.peerId.enumerateArguments(in: line) { - optPeerId = UInt32($0[0]) - } - - // MARK: Routing - - Regex.topology.enumerateArguments(in: line) { - optTopology = $0.first - } - Regex.ifconfig.enumerateArguments(in: line) { - optIfconfig4Arguments = $0 - } - Regex.ifconfig6.enumerateArguments(in: line) { - optIfconfig6Arguments = $0 - } - Regex.route.enumerateArguments(in: line) { - let routeEntryArguments = $0 - - let address = routeEntryArguments[0] - let mask = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : "255.255.255.255" - let gateway = (routeEntryArguments.count > 2) ? routeEntryArguments[2] : nil // defaultGateway4 - optRoutes4.append((address, mask, gateway)) - } - Regex.route6.enumerateArguments(in: line) { - let routeEntryArguments = $0 - - let destinationComponents = routeEntryArguments[0].components(separatedBy: "/") - guard destinationComponents.count == 2 else { - return - } - guard let prefix = UInt8(destinationComponents[1]) else { - return - } - - let destination = destinationComponents[0] - let gateway = (routeEntryArguments.count > 1) ? routeEntryArguments[1] : nil // defaultGateway6 - optRoutes6.append((destination, prefix, gateway)) - } - Regex.gateway.enumerateArguments(in: line) { - optGateway4Arguments = $0 - } - Regex.dns.enumerateArguments(in: line) { - guard $0.count == 2 else { - return - } - optDNSServers.append($0[1]) - } - Regex.domain.enumerateArguments(in: line) { - guard $0.count == 2 else { - return - } - optSearchDomain = $0[1] - } - - // - - if let error = unsupportedError { - throw error - } - } - - // - - strippedLines = optStrippedLines - warning = optWarning - - // MARK: General - - cipher = optCipher - digest = optDigest - compressionFraming = optCompressionFraming - compressionAlgorithm = optCompressionAlgorithm - ca = optCA - clientCertificate = optClientCertificate - clientKey = optClientKey - - if let keyLines = optTLSKeyLines, let strategy = optTLSStrategy { - let optKey: StaticKey? - switch strategy { - case .auth: - optKey = StaticKey(lines: keyLines, direction: optKeyDirection) - - case .crypt: - optKey = StaticKey(lines: keyLines, direction: .client) - } - if let key = optKey { - tlsWrap = SessionProxy.TLSWrap(strategy: strategy, key: key) - } else { - tlsWrap = nil - } - } else { - tlsWrap = nil - } - - keepAliveSeconds = optKeepAliveSeconds - renegotiateAfterSeconds = optRenegotiateAfterSeconds - - // MARK: Client - - optDefaultProto = optDefaultProto ?? .udp - optDefaultPort = optDefaultPort ?? 1194 - if !optRemotes.isEmpty { - hostname = optRemotes[0].0 - - var fullRemotes: [(String, UInt16, SocketType)] = [] - let hostname = optRemotes[0].0 - optRemotes.forEach { - guard $0.0 == hostname else { - return - } - guard let port = $0.1 ?? optDefaultPort else { - return - } - guard let socketType = $0.2 ?? optDefaultProto else { - return - } - fullRemotes.append((hostname, port, socketType)) - } - remotes = fullRemotes - } else { - hostname = nil - remotes = [] - } - - checksEKU = optChecksEKU ?? false - randomizeEndpoint = optRandomizeEndpoint ?? false - - // MARK: Server - - authToken = optAuthToken - peerId = optPeerId - - // MARK: Routing - - // - // excerpts from OpenVPN manpage - // - // "--ifconfig l rn": - // - // Set TUN/TAP adapter parameters. l is the IP address of the local VPN endpoint. For TUN devices in point-to-point mode, rn is the IP address of - // the remote VPN endpoint. For TAP devices, or TUN devices used with --topology subnet, rn is the subnet mask of the virtual network segment which - // is being created or connected to. - // - // "--topology mode": - // - // Note: Using --topology subnet changes the interpretation of the arguments of --ifconfig to mean "address netmask", no longer "local remote". - // - if let ifconfig4Arguments = optIfconfig4Arguments { - guard ifconfig4Arguments.count == 2 else { - throw OptionsError.malformed(option: "ifconfig takes 2 arguments") - } - - let address4: String - let addressMask4: String - let defaultGateway4: String - - let topology = Topology(rawValue: optTopology ?? "") ?? .net30 - switch topology { - case .subnet: - - // default gateway required when topology is subnet - guard let gateway4Arguments = optGateway4Arguments, gateway4Arguments.count == 1 else { - throw OptionsError.malformed(option: "route-gateway takes 1 argument") - } - address4 = ifconfig4Arguments[0] - addressMask4 = ifconfig4Arguments[1] - defaultGateway4 = gateway4Arguments[0] - - default: - address4 = ifconfig4Arguments[0] - addressMask4 = "255.255.255.255" - defaultGateway4 = ifconfig4Arguments[1] - } - let routes4 = optRoutes4.map { IPv4Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway4) } - - ipv4 = IPv4Settings( - address: address4, - addressMask: addressMask4, - defaultGateway: defaultGateway4, - routes: routes4 - ) - } else { - ipv4 = nil - } - - if let ifconfig6Arguments = optIfconfig6Arguments { - guard ifconfig6Arguments.count == 2 else { - throw OptionsError.malformed(option: "ifconfig-ipv6 takes 2 arguments") - } - let address6Components = ifconfig6Arguments[0].components(separatedBy: "/") - guard address6Components.count == 2 else { - throw OptionsError.malformed(option: "ifconfig-ipv6 address must have a /prefix") - } - guard let addressPrefix6 = UInt8(address6Components[1]) else { - throw OptionsError.malformed(option: "ifconfig-ipv6 address prefix must be a 8-bit number") - } - - let address6 = address6Components[0] - let defaultGateway6 = ifconfig6Arguments[1] - let routes6 = optRoutes6.map { IPv6Settings.Route($0.0, $0.1, $0.2 ?? defaultGateway6) } - - ipv6 = IPv6Settings( - address: address6, - addressPrefixLength: addressPrefix6, - defaultGateway: defaultGateway6, - routes: routes6 - ) - } else { - ipv6 = nil - } - - dnsServers = optDNSServers - searchDomain = optSearchDomain - } - - private static func normalizeEncryptedPEMBlock(block: inout [String]) { -// if block.count >= 1 && block[0].contains("ENCRYPTED") { -// return true -// } - - // XXX: restore blank line after encryption header (easier than tweaking trimmedLines) - if block.count >= 3 && block[1].contains("Proc-Type") { - block.insert("", at: 3) -// return true - } -// return false - } -} - -/// Encapsulates the IPv4 settings for the tunnel. -public struct IPv4Settings: CustomStringConvertible { - - /// Represents an IPv4 route in the routing table. - public struct Route: 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: CustomStringConvertible { - - /// Represents an IPv6 route in the routing table. - public struct Route: CustomStringConvertible { - - /// The destination host or subnet. - public let destination: String - - /// The address prefix length. - public let prefixLength: UInt8 - - /// The address of the gateway (uses default gateway if not set). - public let gateway: String? - - fileprivate init(_ destination: String, _ prefixLength: UInt8?, _ gateway: String?) { - self.destination = destination - self.prefixLength = prefixLength ?? 3 - self.gateway = gateway - } - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "{\(destination.maskedDescription)/\(prefixLength) \(gateway?.maskedDescription ?? "default")}" - } - } - - /// The address. - public let address: String - - /// The address prefix length. - public let addressPrefixLength: UInt8 - - /// The address of the default gateway. - public let defaultGateway: String - - /// The additional routes. - public let routes: [Route] - - // MARK: CustomStringConvertible - - /// :nodoc: - public var description: String { - return "addr \(address.maskedDescription)/\(addressPrefixLength) gw \(defaultGateway.maskedDescription) routes \(routes.map { $0.maskedDescription })" - } -} - -private extension SocketType { - init?(protoString: String) { - var str = protoString - if str.hasSuffix("6") { - str.removeLast() - } - self.init(rawValue: str.uppercased()) - } -} diff --git a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift index bc60478..cf66b8d 100644 --- a/TunnelKit/Sources/Core/SessionProxy+Configuration.swift +++ b/TunnelKit/Sources/Core/SessionProxy+Configuration.swift @@ -132,73 +132,93 @@ extension SessionProxy { } } + /// :nodoc: + private struct Fallback { + static let cipher: Cipher = .aes128cbc + + static let digest: Digest = .sha1 + + static let compressionFraming: CompressionFraming = .disabled + } + /// The way to create a `SessionProxy.Configuration` object for a `SessionProxy`. public struct ConfigurationBuilder { - /// - Seealso: `OptionsBundle.cipher` - public var cipher: Cipher + // MARK: General - /// - Seealso: `OptionsBundle.digest` - public var digest: Digest + /// The cipher algorithm for data encryption. + public var cipher: SessionProxy.Cipher? - /// - Seealso: `OptionsBundle.ca` - public let ca: CryptoContainer + /// The digest algorithm for HMAC. + public var digest: SessionProxy.Digest? - /// - Seealso: `OptionsBundle.clientCertificate` + /// Compression framing, disabled by default. + public var compressionFraming: SessionProxy.CompressionFraming? + + /// Compression algorithm, disabled by default. + public var compressionAlgorithm: SessionProxy.CompressionAlgorithm? + + /// The CA for TLS negotiation (PEM format). + public var ca: CryptoContainer? + + /// The optional client certificate for TLS negotiation (PEM format). public var clientCertificate: CryptoContainer? - /// - Seealso: `OptionsBundle.clientKey` + /// The private key for the certificate in `clientCertificate` (PEM format). public var clientKey: CryptoContainer? - /// - Seealso: `OptionsBundle.checksEKU` - public var checksEKU: Bool? + /// The optional TLS wrapping. + public var tlsWrap: SessionProxy.TLSWrap? - /// - Seealso: `OptionsBundle.compressionFraming` - public var compressionFraming: CompressionFraming - - /// - Seealso: `OptionsBundle.compressionAlgorithm` - public var compressionAlgorithm: CompressionAlgorithm? - - /// - Seealso: `OptionsBundle.tlsWrap` - public var tlsWrap: TLSWrap? - - /// - Seealso: `OptionsBundle.keepAliveInterval` + /// Sends periodical keep-alive packets if set. public var keepAliveInterval: TimeInterval? - /// - Seealso: `OptionsBundle.renegotiatesAfter` + /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation. public var renegotiatesAfter: TimeInterval? - /// - Seealso: `OptionsBundle.dnsServers` - public var dnsServers: [String]? + // MARK: Client - /// - Seealso: `OptionsBundle.searchDomain` - public var searchDomain: String? + /// The server hostname (picked from first remote). + public var hostname: String? - /// - Seealso: `OptionsBundle.randomizeEndpoint` + /// The list of server endpoints. + public var endpointProtocols: [EndpointProtocol]? + + /// If true, checks EKU of server certificate. + public var checksEKU: Bool? + + /// Picks endpoint from `remotes` randomly. public var randomizeEndpoint: Bool? /// Server is patched for the PIA VPN provider. public var usesPIAPatches: Bool? - /// :nodoc: - public init(ca: CryptoContainer) { - cipher = .aes128cbc - digest = .sha1 - self.ca = ca - clientCertificate = nil - clientKey = nil - checksEKU = false - compressionFraming = .disabled - compressionAlgorithm = .disabled - tlsWrap = nil - keepAliveInterval = nil - renegotiatesAfter = nil - dnsServers = nil - searchDomain = nil - randomizeEndpoint = false - usesPIAPatches = false - } + // MARK: Server + + /// The auth-token returned by the server. + public var authToken: String? + + /// The peer-id returned by the server. + public var peerId: UInt32? + + // MARK: Routing + + /// The settings for IPv4. `SessionProxy` only evaluates this server-side. + public var ipv4: IPv4Settings? + + /// The settings for IPv6. `SessionProxy` only evaluates this server-side. + public var ipv6: IPv6Settings? + + /// The DNS servers. + public var dnsServers: [String]? + + /// The search domain. + public var searchDomain: String? + /// :nodoc: + public init() { + } + /** Builds a `SessionProxy.Configuration` object. @@ -208,34 +228,63 @@ extension SessionProxy { return Configuration( cipher: cipher, digest: digest, + compressionFraming: compressionFraming, + compressionAlgorithm: compressionAlgorithm, ca: ca, clientCertificate: clientCertificate, clientKey: clientKey, - checksEKU: checksEKU, - compressionFraming: compressionFraming, - compressionAlgorithm: compressionAlgorithm, tlsWrap: tlsWrap, keepAliveInterval: keepAliveInterval, renegotiatesAfter: renegotiatesAfter, - dnsServers: dnsServers, - searchDomain: searchDomain, + hostname: hostname, + endpointProtocols: endpointProtocols, + checksEKU: checksEKU, randomizeEndpoint: randomizeEndpoint, - usesPIAPatches: usesPIAPatches + usesPIAPatches: usesPIAPatches, + authToken: authToken, + peerId: peerId, + ipv4: ipv4, + ipv6: ipv6, + dnsServers: dnsServers, + searchDomain: searchDomain ) } + + // MARK: Shortcuts + + /// :nodoc: + public var fallbackCipher: Cipher { + return cipher ?? Fallback.cipher + } + + /// :nodoc: + public var fallbackDigest: Digest { + return digest ?? Fallback.digest + } + + /// :nodoc: + public var fallbackCompressionFraming: CompressionFraming { + return compressionFraming ?? Fallback.compressionFraming + } } /// The immutable configuration for `SessionProxy`. - public struct Configuration: Codable, Equatable { + public struct Configuration: Codable { /// - Seealso: `SessionProxy.ConfigurationBuilder.cipher` - public let cipher: Cipher + public let cipher: Cipher? /// - Seealso: `SessionProxy.ConfigurationBuilder.digest` - public let digest: Digest + public let digest: Digest? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.compressionFraming` + public let compressionFraming: CompressionFraming? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.compressionAlgorithm` + public let compressionAlgorithm: CompressionAlgorithm? /// - Seealso: `SessionProxy.ConfigurationBuilder.ca` - public let ca: CryptoContainer + public let ca: CryptoContainer? /// - Seealso: `SessionProxy.ConfigurationBuilder.clientCertificate` public let clientCertificate: CryptoContainer? @@ -243,15 +292,6 @@ extension SessionProxy { /// - Seealso: `SessionProxy.ConfigurationBuilder.clientKey` public let clientKey: CryptoContainer? - /// - Seealso: `SessionProxy.ConfigurationBuilder.checksEKU` - public let checksEKU: Bool? - - /// - Seealso: `SessionProxy.ConfigurationBuilder.compressionFraming` - public let compressionFraming: CompressionFraming - - /// - Seealso: `SessionProxy.ConfigurationBuilder.compressionAlgorithm` - public let compressionAlgorithm: CompressionAlgorithm? - /// - Seealso: `SessionProxy.ConfigurationBuilder.tlsWrap` public var tlsWrap: TLSWrap? @@ -261,11 +301,14 @@ extension SessionProxy { /// - Seealso: `SessionProxy.ConfigurationBuilder.renegotiatesAfter` public let renegotiatesAfter: TimeInterval? - /// - Seealso: `SessionProxy.ConfigurationBuilder.dnsServers` - public let dnsServers: [String]? + /// - Seealso: `SessionProxy.ConfigurationBuilder.hostname` + public var hostname: String? - /// - Seealso: `SessionProxy.ConfigurationBuilder.searchDomain` - public let searchDomain: String? + /// - Seealso: `SessionProxy.ConfigurationBuilder.endpointProtocols` + public var endpointProtocols: [EndpointProtocol]? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.checksEKU` + public let checksEKU: Bool? /// - Seealso: `SessionProxy.ConfigurationBuilder.randomizeEndpoint` public let randomizeEndpoint: Bool? @@ -273,49 +316,183 @@ extension SessionProxy { /// - Seealso: `SessionProxy.ConfigurationBuilder.usesPIAPatches` public let usesPIAPatches: Bool? + /// - Seealso: `SessionProxy.ConfigurationBuilder.authToken` + public let authToken: String? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.peerId` + public let peerId: UInt32? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.ipv4` + public let ipv4: IPv4Settings? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.ipv6` + public let ipv6: IPv6Settings? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.dnsServers` + public let dnsServers: [String]? + + /// - Seealso: `SessionProxy.ConfigurationBuilder.searchDomain` + public let searchDomain: String? + /** Returns a `SessionProxy.ConfigurationBuilder` to use this configuration as a starting point for a new one. - Returns: An editable `SessionProxy.ConfigurationBuilder` initialized with this configuration. */ public func builder() -> SessionProxy.ConfigurationBuilder { - var builder = SessionProxy.ConfigurationBuilder(ca: ca) + var builder = SessionProxy.ConfigurationBuilder() builder.cipher = cipher builder.digest = digest - builder.clientCertificate = clientCertificate - builder.clientKey = clientKey - builder.checksEKU = checksEKU builder.compressionFraming = compressionFraming builder.compressionAlgorithm = compressionAlgorithm + builder.ca = ca + builder.clientCertificate = clientCertificate + builder.clientKey = clientKey builder.tlsWrap = tlsWrap builder.keepAliveInterval = keepAliveInterval builder.renegotiatesAfter = renegotiatesAfter - builder.dnsServers = dnsServers - builder.searchDomain = searchDomain + builder.endpointProtocols = endpointProtocols + builder.checksEKU = checksEKU builder.randomizeEndpoint = randomizeEndpoint builder.usesPIAPatches = usesPIAPatches + builder.authToken = authToken + builder.peerId = peerId + builder.ipv4 = ipv4 + builder.ipv6 = ipv6 + builder.dnsServers = dnsServers + builder.searchDomain = searchDomain return builder } - - // MARK: Equatable + + // MARK: Shortcuts /// :nodoc: - public static func ==(lhs: Configuration, rhs: Configuration) -> Bool { - return - (lhs.cipher == rhs.cipher) && - (lhs.digest == rhs.digest) && - (lhs.ca == rhs.ca) && - (lhs.clientCertificate == rhs.clientCertificate) && - (lhs.clientKey == rhs.clientKey) && - (lhs.checksEKU == rhs.checksEKU) && - (lhs.compressionFraming == rhs.compressionFraming) && - (lhs.compressionAlgorithm == rhs.compressionAlgorithm) && - (lhs.keepAliveInterval == rhs.keepAliveInterval) && - (lhs.renegotiatesAfter == rhs.renegotiatesAfter) && - (lhs.dnsServers == rhs.dnsServers) && - (lhs.searchDomain == rhs.searchDomain) && - (lhs.randomizeEndpoint == rhs.randomizeEndpoint) && - (lhs.usesPIAPatches == rhs.usesPIAPatches) + public var fallbackCipher: Cipher { + return cipher ?? Fallback.cipher + } + + /// :nodoc: + public var fallbackDigest: Digest { + return digest ?? Fallback.digest + } + + /// :nodoc: + public var fallbackCompressionFraming: CompressionFraming { + return compressionFraming ?? Fallback.compressionFraming } } } + +/// 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 })" + } +} + +/// :nodoc: +extension EndpointProtocol: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + guard let proto = try EndpointProtocol(rawValue: container.decode(String.self)) else { + throw ConfigurationError.malformed(option: "remote/proto") + } + self.init(proto.socketType, proto.port) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} diff --git a/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift b/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift index 89b4064..2cb1ccb 100644 --- a/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift +++ b/TunnelKit/Sources/Core/SessionProxy+SessionReply.swift @@ -41,19 +41,16 @@ import Foundation public protocol SessionReply { /// The returned options. - var options: OptionsBundle { get } + var options: SessionProxy.Configuration { get } } extension SessionProxy { - - // XXX: parsing is very optimistic - struct PushReply: SessionReply, CustomStringConvertible { private static let prefix = "PUSH_REPLY," private let original: String - let options: OptionsBundle + let options: SessionProxy.Configuration init?(message: String) throws { guard message.hasPrefix(PushReply.prefix) else { @@ -65,14 +62,14 @@ extension SessionProxy { original = String(message[prefixIndex...]) let lines = original.components(separatedBy: ",") - options = try OptionsBundle(from: lines) + options = try ConfigurationParser.parsed(fromLines: lines).configuration } // MARK: CustomStringConvertible var description: String { let stripped = NSMutableString(string: original) - OptionsBundle.Regex.authToken.replaceMatches( + ConfigurationParser.Regex.authToken.replaceMatches( in: stripped, options: [], range: NSMakeRange(0, stripped.length), diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index 95d2b7a..5f278ce 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -86,7 +86,7 @@ public class SessionProxy { private var keepAliveInterval: TimeInterval? { let interval: TimeInterval? - if let negInterval = pushReply?.options.keepAliveSeconds, negInterval > 0 { + if let negInterval = pushReply?.options.keepAliveInterval, negInterval > 0 { interval = TimeInterval(negInterval) } else if let cfgInterval = configuration.keepAliveInterval, cfgInterval > 0.0 { interval = cfgInterval @@ -179,6 +179,10 @@ public class SessionProxy { - Parameter configuration: The `SessionProxy.Configuration` to use for this session. */ public init(queue: DispatchQueue, configuration: Configuration, cachesURL: URL) throws { + guard let ca = configuration.ca else { + throw ConfigurationError.missingConfiguration(option: "ca") + } + self.queue = queue self.configuration = configuration self.cachesURL = cachesURL @@ -192,7 +196,7 @@ public class SessionProxy { if let tlsWrap = configuration.tlsWrap { switch tlsWrap.strategy { case .auth: - controlChannel = try ControlChannel(withAuthKey: tlsWrap.key, digest: configuration.digest) + controlChannel = try ControlChannel(withAuthKey: tlsWrap.key, digest: configuration.fallbackDigest) case .crypt: controlChannel = try ControlChannel(withCryptKey: tlsWrap.key) @@ -203,7 +207,7 @@ public class SessionProxy { // cache PEMs locally (mandatory for OpenSSL) let fm = FileManager.default - try configuration.ca.pem.write(to: caURL, atomically: true, encoding: .ascii) + try ca.pem.write(to: caURL, atomically: true, encoding: .ascii) if let container = configuration.clientCertificate { try container.pem.write(to: clientCertificateURL, atomically: true, encoding: .ascii) } else { @@ -630,8 +634,8 @@ public class SessionProxy { log.debug("CA MD5 is: \(caMD5)") return try? PIAHardReset( caMd5Digest: caMD5, - cipher: configuration.cipher, - digest: configuration.digest + cipher: configuration.fallbackCipher, + digest: configuration.fallbackDigest ).encodedData() } return nil @@ -1036,6 +1040,10 @@ public class SessionProxy { log.debug("Set up encryption") } + let pushedCipher = pushReply.options.cipher + if let negCipher = pushedCipher { + log.info("\tNegotiated cipher: \(negCipher.rawValue)") + } let pushedFraming = pushReply.options.compressionFraming if let negFraming = pushedFraming { log.info("\tNegotiated compression framing: \(negFraming)") @@ -1044,19 +1052,15 @@ public class SessionProxy { if let negCompression = pushedCompression { log.info("\tNegotiated compression algorithm: \(negCompression)") } - if let negPing = pushReply.options.keepAliveSeconds { + if let negPing = pushReply.options.keepAliveInterval { log.info("\tNegotiated keep-alive: \(negPing) seconds") } - let pushedCipher = pushReply.options.cipher - if let negCipher = pushedCipher { - log.info("\tNegotiated cipher: \(negCipher.rawValue)") - } let bridge: EncryptionBridge do { bridge = try EncryptionBridge( - pushedCipher ?? configuration.cipher, - configuration.digest, + pushedCipher ?? configuration.fallbackCipher, + configuration.fallbackDigest, auth, sessionId, remoteSessionId @@ -1070,7 +1074,7 @@ public class SessionProxy { encrypter: bridge.encrypter(), decrypter: bridge.decrypter(), peerId: pushReply.options.peerId ?? PacketPeerIdDisabled, - compressionFraming: (pushedFraming ?? configuration.compressionFraming).native, + compressionFraming: (pushedFraming ?? configuration.fallbackCompressionFraming).native, compressionAlgorithm: (pushedCompression ?? configuration.compressionAlgorithm ?? .disabled).native, maxPackets: link?.packetBufferSize ?? 200, usesReplayProtection: CoreConfiguration.usesReplayProtection diff --git a/TunnelKitTests/AppExtensionTests.swift b/TunnelKitTests/AppExtensionTests.swift index 6e14e7f..ff212e3 100644 --- a/TunnelKitTests/AppExtensionTests.swift +++ b/TunnelKitTests/AppExtensionTests.swift @@ -60,9 +60,11 @@ class AppExtensionTests: XCTestCase { let hostname = "example.com" let credentials = SessionProxy.Credentials("foo", "bar") - var sessionBuilder = SessionProxy.ConfigurationBuilder(ca: CryptoContainer(pem: "abcdef")) + var sessionBuilder = SessionProxy.ConfigurationBuilder() + sessionBuilder.ca = CryptoContainer(pem: "abcdef") sessionBuilder.cipher = .aes128cbc sessionBuilder.digest = .sha256 + sessionBuilder.endpointProtocols = [] builder = TunnelKitProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) XCTAssertNotNil(builder) @@ -82,9 +84,9 @@ class AppExtensionTests: XCTestCase { let K = TunnelKitProvider.Configuration.Keys.self XCTAssertEqual(proto?.providerConfiguration?[K.appGroup] as? String, appGroup) - XCTAssertEqual(proto?.providerConfiguration?[K.cipherAlgorithm] as? String, cfg.sessionConfiguration.cipher.rawValue) - XCTAssertEqual(proto?.providerConfiguration?[K.digestAlgorithm] as? String, cfg.sessionConfiguration.digest.rawValue) - XCTAssertEqual(proto?.providerConfiguration?[K.ca] as? String, cfg.sessionConfiguration.ca.pem) + XCTAssertEqual(proto?.providerConfiguration?[K.cipherAlgorithm] as? String, cfg.sessionConfiguration.cipher?.rawValue) + XCTAssertEqual(proto?.providerConfiguration?[K.digestAlgorithm] as? String, cfg.sessionConfiguration.digest?.rawValue) + XCTAssertEqual(proto?.providerConfiguration?[K.ca] as? String, cfg.sessionConfiguration.ca?.pem) XCTAssertEqual(proto?.providerConfiguration?[K.mtu] as? Int, cfg.mtu) XCTAssertEqual(proto?.providerConfiguration?[K.renegotiatesAfter] as? TimeInterval, cfg.sessionConfiguration.renegotiatesAfter) XCTAssertEqual(proto?.providerConfiguration?[K.debug] as? Bool, cfg.shouldDebug) diff --git a/TunnelKitTests/ConfigurationParserTests.swift b/TunnelKitTests/ConfigurationParserTests.swift index d2c6fc1..034b4a0 100644 --- a/TunnelKitTests/ConfigurationParserTests.swift +++ b/TunnelKitTests/ConfigurationParserTests.swift @@ -27,6 +27,8 @@ import XCTest import TunnelKit class ConfigurationParserTests: XCTestCase { + let base: [String] = ["", "", "remote 1.2.3.4"] + override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. @@ -37,12 +39,41 @@ class ConfigurationParserTests: XCTestCase { super.tearDown() } + // from lines + + func testCompression() throws { +// XCTAssertNotNil(try OptionsBundle.parsed(fromLines: base + ["comp-lzo"]).warning) + XCTAssertNil(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo"]).warning) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo no"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo yes"])) +// XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: base + ["comp-lzo yes"])) + + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress"])) + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: base + ["compress lzo"])) + } + + func testDHCPOption() throws { + let lines = base + ["dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com"] + XCTAssertNoThrow(try ConfigurationParser.parsed(fromLines: lines)) + + let parsed = try! ConfigurationParser.parsed(fromLines: lines).configuration + XCTAssertEqual(parsed.dnsServers, ["8.8.8.8", "ffff::1"]) + XCTAssertEqual(parsed.searchDomain, "example.com") + } + + func testConnectionBlock() throws { + let lines = base + ["", ""] + XCTAssertThrowsError(try ConfigurationParser.parsed(fromLines: lines)) + } + + // from file + func testPIA() throws { let file = try ConfigurationParser.parsed(fromURL: url(withName: "pia-hungary")) - XCTAssertEqual(file.hostname, "hungary.privateinternetaccess.com") + XCTAssertEqual(file.configuration.hostname, "hungary.privateinternetaccess.com") XCTAssertEqual(file.configuration.cipher, .aes128cbc) XCTAssertEqual(file.configuration.digest, .sha1) - XCTAssertEqual(file.protocols, [ + XCTAssertEqual(file.configuration.endpointProtocols, [ EndpointProtocol(.udp, 1198), EndpointProtocol(.tcp, 502) ]) diff --git a/TunnelKitTests/OptionsBundleTests.swift b/TunnelKitTests/OptionsBundleTests.swift deleted file mode 100644 index 66af67d..0000000 --- a/TunnelKitTests/OptionsBundleTests.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// OptionsBundleTests.swift -// TunnelKitTests -// -// Created by Davide De Rosa on 4/3/19. -// Copyright (c) 2019 Davide De Rosa. All rights reserved. -// -// https://github.com/keeshux -// -// This file is part of TunnelKit. -// -// TunnelKit is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// TunnelKit is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with TunnelKit. If not, see . -// - -import XCTest -import TunnelKit - -class OptionsBundleTests: XCTestCase { - let base: [String] = ["", "", "remote 1.2.3.4"] - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testCompression() throws { -// XCTAssertNotNil(try OptionsBundle.parsed(fromLines: base + ["comp-lzo"]).warning) - XCTAssertNil(try OptionsBundle(from: base + ["comp-lzo"]).warning) - XCTAssertNoThrow(try OptionsBundle(from: base + ["comp-lzo no"])) - XCTAssertNoThrow(try OptionsBundle(from: base + ["comp-lzo yes"])) -// XCTAssertThrowsError(try OptionsBundle(from: base + ["comp-lzo yes"])) - - XCTAssertNoThrow(try OptionsBundle(from: base + ["compress"])) - XCTAssertNoThrow(try OptionsBundle(from: base + ["compress lzo"])) - } - - func testDHCPOption() throws { - let lines = base + ["dhcp-option DNS 8.8.8.8", "dhcp-option DNS6 ffff::1", "dhcp-option DOMAIN example.com"] - XCTAssertNoThrow(try OptionsBundle(from: lines)) - - let parsed = try! OptionsBundle(from: lines) - XCTAssertEqual(parsed.dnsServers, ["8.8.8.8", "ffff::1"]) - XCTAssertEqual(parsed.searchDomain, "example.com") - } - - func testConnectionBlock() throws { - let lines = base + ["", ""] - XCTAssertThrowsError(try OptionsBundle(from: lines)) - } -} diff --git a/TunnelKitTests/PushTests.swift b/TunnelKitTests/PushTests.swift index d4963ce..9c4495b 100644 --- a/TunnelKitTests/PushTests.swift +++ b/TunnelKitTests/PushTests.swift @@ -28,11 +28,11 @@ import XCTest private extension SessionReply { func debug() { - print("Compression framing: \(options.compressionFraming?.description ?? "none")") - print("Compression algorithm: \(options.compressionAlgorithm?.description ?? "none")") + print("Compression framing: \(options.compressionFraming?.description ?? "disabled")") + print("Compression algorithm: \(options.compressionAlgorithm?.description ?? "disabled")") 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) } }