diff --git a/CHANGELOG.md b/CHANGELOG.md index d15a4c2..ae9dfe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - OpenVPN: Support for `--route-nopull`. [#280](https://github.com/passepartoutvpn/tunnelkit/pull/280) +- OpenVPN: Support for `--remote-random-hostname`. [#286](https://github.com/passepartoutvpn/tunnelkit/pull/286) ### Changed diff --git a/Sources/TunnelKitCore/Endpoint.swift b/Sources/TunnelKitCore/Endpoint.swift index 5f2b11b..d1f0904 100644 --- a/Sources/TunnelKitCore/Endpoint.swift +++ b/Sources/TunnelKitCore/Endpoint.swift @@ -31,10 +31,39 @@ public struct Endpoint: RawRepresentable, Codable, Equatable, CustomStringConver public let proto: EndpointProtocol - public init(_ address: String, _ proto: EndpointProtocol) { + public init(_ address: String, _ proto: EndpointProtocol) { self.address = address self.proto = proto } + + public var isIPv4: Bool { + var addr = in_addr() + let result = address.withCString { + inet_pton(AF_INET, $0, &addr) + } + return result > 0 + } + + public var isIPv6: Bool { + var addr = in_addr() + let result = address.withCString { + inet_pton(AF_INET6, $0, &addr) + } + return result > 0 + } + + public var isHostname: Bool { + !isIPv4 && !isIPv6 + } + + public func withRandomPrefixLength(_ length: Int) throws -> Endpoint { + guard isHostname else { + return self + } + let prefix = try SecureRandom.data(length: length) + let prefixedAddress = "\(prefix.toHex()).\(address)" + return Endpoint(prefixedAddress, proto) + } // MARK: RawRepresentable diff --git a/Sources/TunnelKitOpenVPNAppExtension/ConnectionStrategy.swift b/Sources/TunnelKitOpenVPNAppExtension/ConnectionStrategy.swift index f640b64..da64f81 100644 --- a/Sources/TunnelKitOpenVPNAppExtension/ConnectionStrategy.swift +++ b/Sources/TunnelKitOpenVPNAppExtension/ConnectionStrategy.swift @@ -57,12 +57,9 @@ class ConnectionStrategy { } init(configuration: OpenVPN.Configuration) { - guard var remotes = configuration.remotes, !remotes.isEmpty else { + guard let remotes = configuration.processedRemotes, !remotes.isEmpty else { fatalError("No remotes provided") } - if configuration.randomizeEndpoint ?? false { - remotes.shuffle() - } self.remotes = remotes.map(ResolvedRemote.init) currentRemoteIndex = 0 } diff --git a/Sources/TunnelKitOpenVPNCore/Configuration.swift b/Sources/TunnelKitOpenVPNCore/Configuration.swift index bb9a6a1..9a6e8b6 100644 --- a/Sources/TunnelKitOpenVPNCore/Configuration.swift +++ b/Sources/TunnelKitOpenVPNCore/Configuration.swift @@ -229,6 +229,9 @@ extension OpenVPN { /// Picks endpoint from `remotes` randomly. public var randomizeEndpoint: Bool? + /// Prepend hostnames with a number of random bytes defined in `Configuration.randomHostnamePrefixLength`. + public var randomizeHostnames: Bool? + /// Server is patched for the PIA VPN provider. public var usesPIAPatches: Bool? @@ -345,6 +348,7 @@ extension OpenVPN { checksSANHost: checksSANHost, sanHost: sanHost, randomizeEndpoint: randomizeEndpoint, + randomizeHostnames: randomizeHostnames, usesPIAPatches: usesPIAPatches, mtu: mtu, authUserPass: authUserPass, @@ -381,6 +385,8 @@ extension OpenVPN { static let compressionAlgorithm: CompressionAlgorithm = .disabled } + private static let randomHostnamePrefixLength = 6 + /// - Seealso: `ConfigurationBuilder.cipher` public let cipher: Cipher? @@ -438,6 +444,9 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.randomizeEndpoint` public let randomizeEndpoint: Bool? + /// - Seealso: `ConfigurationBuilder.randomizeHostnames` + public var randomizeHostnames: Bool? + /// - Seealso: `ConfigurationBuilder.usesPIAPatches` public let usesPIAPatches: Bool? @@ -524,6 +533,28 @@ extension OpenVPN { let pulled = Array(Set(all).subtracting(notPulled)) return !pulled.isEmpty ? pulled : nil } + + // MARK: Computed + + public var processedRemotes: [Endpoint]? { + guard var processedRemotes = remotes else { + return nil + } + if randomizeEndpoint ?? false { + processedRemotes.shuffle() + } + if let randomPrefixLength = (randomizeHostnames ?? false) ? Self.randomHostnamePrefixLength : nil { + processedRemotes = processedRemotes.compactMap { + do { + return try $0.withRandomPrefixLength(randomPrefixLength) + } catch { + log.warning("Could not prepend random prefix: \(error)") + return nil + } + } + } + return processedRemotes + } } } @@ -558,6 +589,7 @@ extension OpenVPN.Configuration { builder.checksSANHost = checksSANHost builder.sanHost = sanHost builder.randomizeEndpoint = randomizeEndpoint + builder.randomizeHostnames = randomizeHostnames builder.usesPIAPatches = usesPIAPatches builder.mtu = mtu builder.authUserPass = authUserPass @@ -638,6 +670,9 @@ extension OpenVPN.Configuration { if randomizeEndpoint ?? false { log.info("\tRandomize endpoint: true") } + if randomizeHostnames ?? false { + log.info("\tRandomize hostnames: true") + } if let routingPolicies = routingPolicies { log.info("\tGateway: \(routingPolicies.map { $0.rawValue })") } else { diff --git a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift index 6116189..c9e4d1b 100644 --- a/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift +++ b/Sources/TunnelKitOpenVPNCore/ConfigurationParser.swift @@ -85,6 +85,8 @@ extension OpenVPN { static let remoteRandom = NSRegularExpression("^remote-random") + static let remoteRandomHostname = NSRegularExpression("^remote-random-hostname") + static let mtu = NSRegularExpression("^tun-mtu +\\d+") // MARK: Server @@ -123,7 +125,7 @@ extension OpenVPN { // MARK: Unsupported - // static let fragment = NSRegularExpression("^fragment +\\d+") +// static let fragment = NSRegularExpression("^fragment +\\d+") static let fragment = NSRegularExpression("^fragment") static let connectionProxy = NSRegularExpression("^\\w+-proxy") @@ -274,6 +276,7 @@ extension OpenVPN { var authUserPass = false var optChecksEKU: Bool? var optRandomizeEndpoint: Bool? + var optRandomizeHostnames: Bool? var optMTU: Int? // var optAuthToken: String? @@ -574,6 +577,10 @@ extension OpenVPN { isHandled = true optRandomizeEndpoint = true } + Regex.remoteRandomHostname.enumerateComponents(in: line) { _ in + isHandled = true + optRandomizeHostnames = true + } Regex.mtu.enumerateArguments(in: line) { isHandled = true guard let str = $0.first else { @@ -796,6 +803,7 @@ extension OpenVPN { sessionBuilder.authUserPass = authUserPass sessionBuilder.checksEKU = optChecksEKU sessionBuilder.randomizeEndpoint = optRandomizeEndpoint + sessionBuilder.randomizeHostnames = optRandomizeHostnames sessionBuilder.mtu = optMTU sessionBuilder.xorMask = optXorMask diff --git a/Tests/TunnelKitOpenVPNTests/ConfigurationTests.swift b/Tests/TunnelKitOpenVPNTests/ConfigurationTests.swift new file mode 100644 index 0000000..b75bf73 --- /dev/null +++ b/Tests/TunnelKitOpenVPNTests/ConfigurationTests.swift @@ -0,0 +1,70 @@ +// +// ConfigurationTests.swift +// TunnelKitOpenVPNTests +// +// Created by Davide De Rosa on 10/17/22. +// Copyright (c) 2022 Davide De Rosa. All rights reserved. +// +// https://github.com/passepartoutvpn +// +// 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 TunnelKitCore +import TunnelKitOpenVPNCore + +class ConfigurationTests: XCTestCase { + override func setUp() { + super.setUp() + + CoreConfiguration.masksPrivateData = false + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testRandomizeHostnames() { + var builder = OpenVPN.ConfigurationBuilder() + let hostname = "my.host.name" + let ipv4 = "1.2.3.4" + builder.remotes = [ + .init(hostname, .init(.udp, 1111)), + .init(ipv4, .init(.udp4, 3333)) + ] + builder.randomizeHostnames = true + let cfg = builder.build() + + cfg.processedRemotes?.forEach { + let comps = $0.address.components(separatedBy: ".") + guard let first = comps.first else { + XCTFail() + return + } + if $0.isHostname { + XCTAssert($0.address.hasSuffix(hostname)) + XCTAssert(first.count == 12) + XCTAssert(first.allSatisfy { + "0123456789abcdef".contains($0) + }) + } else { + XCTAssert($0.address == ipv4) + } + } + } +}