diff --git a/CHANGELOG.md b/CHANGELOG.md index ce879bb..21269e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Support for XOR patch (Sam Foxman). [#170](https://github.com/passepartoutvpn/tunnelkit/pull/170) + ## 3.3.3 (2021-07-19) ### Added diff --git a/README.md b/README.md index 8c3993f..6832a64 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,16 @@ The library therefore supports compression framing, just not newer compression. ### Support for .ovpn configuration -TunnelKit can parse .ovpn configuration files. Below are a few limitations worth mentioning. +TunnelKit can parse .ovpn configuration files. Below are a few details worth mentioning. -Unsupported: +#### Non-standard + +- Single-byte XOR masking + - Via `--scramble xormask ` + - XOR all incoming and outgoing bytes by the ASCII value of the character argument + - See [Tunnelblick website][about-tunnelblick-xor] for more details + +#### Unsupported - UDP fragmentation, i.e. `--fragment` - Compression via `--compress` other than empty or `lzo` @@ -51,7 +58,7 @@ Unsupported: - `` blocks - `vpn_gateway` and `net_gateway` literals in routes -Ignored: +#### Ignored - Some MTU overrides - `--link-mtu` and variants @@ -201,8 +208,9 @@ For more details please see [CONTRIBUTING][contrib-readme]. - [lzo][dep-lzo-website] - Copyright (c) 1996-2017 Markus F.X.J. Oberhumer - [PIATunnel][dep-piatunnel-repo] - Copyright (c) 2018-Present Private Internet Access -- [SURFnet][surfnet] +- [SURFnet][ppl-surfnet] - [SwiftyBeaver][dep-swiftybeaver-repo] - Copyright (c) 2015 Sebastian Kreutzberger +- [XMB5][ppl-xmb5] for the [XOR patch][ppl-xmb5-xor] - Copyright (c) 2020 Sam Foxman This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. ([https://www.openssl.org/][dep-openssl]) @@ -236,7 +244,10 @@ Website: [passepartoutvpn.app][about-website] [dep-piatunnel-repo]: https://github.com/pia-foss/tunnel-apple [dep-swiftybeaver-repo]: https://github.com/SwiftyBeaver/SwiftyBeaver [dep-lzo-website]: http://www.oberhumer.com/opensource/lzo/ -[surfnet]: https://www.surf.nl/en/about-surf/subsidiaries/surfnet +[ppl-surfnet]: https://www.surf.nl/en/about-surf/subsidiaries/surfnet +[ppl-xmb5]: https://github.com/XMB5 +[ppl-xmb5-xor]: https://github.com/passepartoutvpn/tunnelkit/pull/170 +[about-tunnelblick-xor]: https://tunnelblick.net/cOpenvpn_xorpatch.html [about-twitter]: https://twitter.com/keeshux [about-website]: https://passepartoutvpn.app diff --git a/TunnelKit/Sources/AppExtension/LinkProducer.swift b/TunnelKit/Sources/AppExtension/LinkProducer.swift index 0c76bc9..371500e 100644 --- a/TunnelKit/Sources/AppExtension/LinkProducer.swift +++ b/TunnelKit/Sources/AppExtension/LinkProducer.swift @@ -30,6 +30,8 @@ public protocol LinkProducer { /** Returns a `LinkInterface`. + + - Parameter xorMask: The XOR mask. **/ - func link() -> LinkInterface + func link(xorMask: UInt8?) -> LinkInterface } diff --git a/TunnelKit/Sources/Core/LinkInterface.swift b/TunnelKit/Sources/Core/LinkInterface.swift index 077047a..c7a7f9f 100644 --- a/TunnelKit/Sources/Core/LinkInterface.swift +++ b/TunnelKit/Sources/Core/LinkInterface.swift @@ -47,4 +47,7 @@ public protocol LinkInterface: IOInterface { /// The number of packets that this interface is able to bufferize. var packetBufferSize: Int { get } + + /// A byte to xor all packet payloads with. + var xorMask: UInt8 { get } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NETCPLink.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NETCPLink.swift index b15e2f9..61e10d1 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NETCPLink.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NETCPLink.swift @@ -32,9 +32,12 @@ class NETCPLink: LinkInterface { private let maxPacketSize: Int - init(impl: NWTCPConnection, maxPacketSize: Int? = nil) { + let xorMask: UInt8 + + init(impl: NWTCPConnection, maxPacketSize: Int? = nil, xorMask: UInt8?) { self.impl = impl self.maxPacketSize = maxPacketSize ?? (512 * 1024) + self.xorMask = xorMask ?? 0 } // MARK: LinkInterface @@ -57,7 +60,7 @@ class NETCPLink: LinkInterface { // WARNING: runs in Network.framework queue impl.readMinimumLength(2, maximumLength: packetBufferSize) { [weak self] (data, error) in - guard let _ = self else { + guard let self = self else { return } queue.sync { @@ -69,9 +72,9 @@ class NETCPLink: LinkInterface { var newBuffer = buffer newBuffer.append(contentsOf: data) var until = 0 - let packets = PacketStream.packets(fromStream: newBuffer, until: &until) + let packets = PacketStream.packets(fromStream: newBuffer, until: &until, xorMask: self.xorMask) newBuffer = newBuffer.subdata(in: until.. Void)?) { - let stream = PacketStream.stream(fromPacket: packet) + let stream = PacketStream.stream(fromPacket: packet, xorMask: xorMask) impl.write(stream) { (error) in completionHandler?(error) } } func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { - let stream = PacketStream.stream(fromPackets: packets) + let stream = PacketStream.stream(fromPackets: packets, xorMask: xorMask) impl.write(stream) { (error) in completionHandler?(error) } @@ -95,7 +98,7 @@ class NETCPLink: LinkInterface { /// :nodoc: extension NETCPSocket: LinkProducer { - public func link() -> LinkInterface { - return NETCPLink(impl: impl) + public func link(xorMask: UInt8?) -> LinkInterface { + return NETCPLink(impl: impl, maxPacketSize: nil, xorMask: xorMask) } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NEUDPLink.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NEUDPLink.swift index 73d7b3c..ebd7068 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NEUDPLink.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/NEUDPLink.swift @@ -31,9 +31,12 @@ class NEUDPLink: LinkInterface { private let maxDatagrams: Int - init(impl: NWUDPSession, maxDatagrams: Int? = nil) { + let xorMask: UInt8 + + init(impl: NWUDPSession, maxDatagrams: Int? = nil, xorMask: UInt8?) { self.impl = impl self.maxDatagrams = maxDatagrams ?? 200 + self.xorMask = xorMask ?? 0 } // MARK: LinkInterface @@ -51,24 +54,46 @@ class NEUDPLink: LinkInterface { func setReadHandler(queue: DispatchQueue, _ handler: @escaping ([Data]?, Error?) -> Void) { // WARNING: runs in Network.framework queue - impl.setReadHandler({ [weak self] (packets, error) in - guard let _ = self else { + impl.setReadHandler({ [weak self] packets, error in + guard let self = self else { return } - queue.sync { - handler(packets, error) + var packetsToUse: [Data]? + if let packets = packets, self.xorMask != 0 { + packetsToUse = packets.map { packet in + Data(bytes: packet.map { $0 ^ self.xorMask }, count: packet.count) + } + } else { + packetsToUse = packets } - }, maxDatagrams: maxDatagrams) + queue.sync { + handler(packetsToUse, error) + } + }, maxDatagrams: maxDatagrams) } func writePacket(_ packet: Data, completionHandler: ((Error?) -> Void)?) { - impl.writeDatagram(packet) { (error) in + var dataToUse: Data + if xorMask != 0 { + dataToUse = Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count) + } else { + dataToUse = packet + } + impl.writeDatagram(dataToUse) { error in completionHandler?(error) } } func writePackets(_ packets: [Data], completionHandler: ((Error?) -> Void)?) { - impl.writeMultipleDatagrams(packets) { (error) in + var packetsToUse: [Data] + if xorMask != 0 { + packetsToUse = packets.map { packet in + Data(bytes: packet.map { $0 ^ xorMask }, count: packet.count) + } + } else { + packetsToUse = packets + } + impl.writeMultipleDatagrams(packetsToUse) { error in completionHandler?(error) } } @@ -76,7 +101,7 @@ class NEUDPLink: LinkInterface { /// :nodoc: extension NEUDPSocket: LinkProducer { - public func link() -> LinkInterface { - return NEUDPLink(impl: impl) + public func link(xorMask: UInt8?) -> LinkInterface { + return NEUDPLink(impl: impl, maxDatagrams: nil, xorMask: xorMask) } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift index 9aef1b1..b33e6aa 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift @@ -454,10 +454,10 @@ extension OpenVPNTunnelProvider: GenericSocketDelegate { return } if session.canRebindLink() { - session.rebindLink(producer.link()) + session.rebindLink(producer.link(xorMask: cfg.sessionConfiguration.xorMask)) reasserting = false } else { - session.setLink(producer.link()) + session.setLink(producer.link(xorMask: cfg.sessionConfiguration.xorMask)) } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift b/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift index f3a2873..9f743a4 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift @@ -216,6 +216,9 @@ extension OpenVPN { /// The number of seconds after which a renegotiation should be initiated. If `nil`, the client will never initiate a renegotiation. public var renegotiatesAfter: TimeInterval? + /// A byte to xor all packet payloads with. + public var xorMask: UInt8? + // MARK: Client /// The server hostname (picked from first remote). @@ -324,6 +327,7 @@ extension OpenVPN { keepAliveInterval: keepAliveInterval, keepAliveTimeout: keepAliveTimeout, renegotiatesAfter: renegotiatesAfter, + xorMask: xorMask, hostname: hostname, endpointProtocols: endpointProtocols, checksEKU: checksEKU, @@ -414,6 +418,9 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.renegotiatesAfter` public let renegotiatesAfter: TimeInterval? + /// - Seealso: `ConfigurationBuilder.xorMask` + public let xorMask: UInt8? + /// - Seealso: `ConfigurationBuilder.hostname` public let hostname: String? @@ -545,6 +552,7 @@ extension OpenVPN.Configuration { builder.proxyAutoConfigurationURL = proxyAutoConfigurationURL builder.proxyBypassDomains = proxyBypassDomains builder.routingPolicies = routingPolicies + builder.xorMask = xorMask return builder } } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/ConfigurationParser.swift b/TunnelKit/Sources/Protocols/OpenVPN/ConfigurationParser.swift index fbf4293..44b89c6 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/ConfigurationParser.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/ConfigurationParser.swift @@ -60,6 +60,8 @@ extension OpenVPN { static let renegSec = NSRegularExpression("^reneg-sec +\\d+") + static let xorMask = NSRegularExpression("^scramble +xormask +.$") + static let blockBegin = NSRegularExpression("^<[\\w\\-]+>") static let blockEnd = NSRegularExpression("^<\\/[\\w\\-]+>") @@ -218,6 +220,7 @@ extension OpenVPN { var optKeepAliveSeconds: TimeInterval? var optKeepAliveTimeoutSeconds: TimeInterval? var optRenegotiateAfterSeconds: TimeInterval? + var optXorMask: UInt8? // var optDefaultProto: SocketType? var optDefaultPort: UInt16? @@ -459,6 +462,13 @@ extension OpenVPN { } optRenegotiateAfterSeconds = TimeInterval(arg) } + Regex.xorMask.enumerateArguments(in: line) { + isHandled = true + if $0.count != 2 { + return + } + optXorMask = Character($0[1]).asciiValue + } // MARK: Client @@ -726,6 +736,7 @@ extension OpenVPN { sessionBuilder.checksEKU = optChecksEKU sessionBuilder.randomizeEndpoint = optRandomizeEndpoint sessionBuilder.mtu = optMTU + sessionBuilder.xorMask = optXorMask // MARK: Server diff --git a/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.h b/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.h index 7ef3c98..9e112c1 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.h +++ b/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.h @@ -29,9 +29,9 @@ NS_ASSUME_NONNULL_BEGIN @interface PacketStream : NSObject -+ (NSArray *)packetsFromStream:(NSData *)stream until:(nullable NSInteger *)until; -+ (NSData *)streamFromPacket:(NSData *)packet; -+ (NSData *)streamFromPackets:(NSArray *)packets; ++ (NSArray *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask; ++ (NSData *)streamFromPacket:(NSData *)packet xorMask:(uint8_t)xorMask; ++ (NSData *)streamFromPackets:(NSArray *)packets xorMask:(uint8_t)xorMask; @end diff --git a/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.m b/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.m index 07e1865..24d1f36 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.m +++ b/TunnelKit/Sources/Protocols/OpenVPN/PacketStream.m @@ -29,7 +29,18 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); @implementation PacketStream -+ (NSArray *)packetsFromStream:(NSData *)stream until:(NSInteger *)until ++ (void)memcpyXor:(uint8_t *)dst src:(NSData *)src xorMask:(uint8_t)xorMask +{ + if (xorMask != 0) { + for (int i = 0; i < src.length; ++i) { + dst[i] = ((uint8_t *)(src.bytes))[i] ^ xorMask; + } + return; + } + memcpy(dst, src.bytes, src.length); +} + ++ (NSArray *)packetsFromStream:(NSData *)stream until:(NSInteger *)until xorMask:(uint8_t)xorMask { NSInteger ni = 0; NSMutableArray *parsed = [[NSMutableArray alloc] init]; @@ -42,6 +53,12 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); break; } NSData *packet = [stream subdataWithRange:NSMakeRange(start, packlen)]; + uint8_t* packetBytes = (uint8_t*) packet.bytes; + if (xorMask != 0) { + for (int i = 0; i < packet.length; i++) { + packetBytes[i] ^= xorMask; + } + } [parsed addObject:packet]; ni = end; } @@ -51,19 +68,19 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); return parsed; } -+ (NSData *)streamFromPacket:(NSData *)packet ++ (NSData *)streamFromPacket:(NSData *)packet xorMask:(uint8_t)xorMask { NSMutableData *raw = [[NSMutableData alloc] initWithLength:(PacketStreamHeaderLength + packet.length)]; uint8_t *ptr = raw.mutableBytes; *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); ptr += PacketStreamHeaderLength; - memcpy(ptr, packet.bytes, packet.length); + [PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; return raw; } -+ (NSData *)streamFromPackets:(NSArray *)packets; ++ (NSData *)streamFromPackets:(NSArray *)packets xorMask:(uint8_t)xorMask { NSInteger streamLength = 0; for (NSData *p in packets) { @@ -75,7 +92,7 @@ static const NSInteger PacketStreamHeaderLength = sizeof(uint16_t); for (NSData *packet in packets) { *(uint16_t *)ptr = CFSwapInt16HostToBig(packet.length); ptr += PacketStreamHeaderLength; - memcpy(ptr, packet.bytes, packet.length); + [PacketStream memcpyXor:ptr src:packet xorMask:xorMask]; ptr += packet.length; } return raw; diff --git a/TunnelKit/Tests/OpenVPN/ConfigurationParserTests.swift b/TunnelKit/Tests/OpenVPN/ConfigurationParserTests.swift index 320982c..920e36b 100644 --- a/TunnelKit/Tests/OpenVPN/ConfigurationParserTests.swift +++ b/TunnelKit/Tests/OpenVPN/ConfigurationParserTests.swift @@ -114,6 +114,16 @@ class ConfigurationParserTests: XCTestCase { try privateTestEncryptedCertificateKey(pkcs: "8") } + func testXOR() throws { + let cfg = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask F"]) + XCTAssertNil(cfg.warning) + XCTAssertEqual(cfg.configuration.xorMask, Character("F").asciiValue) + + let cfg2 = try OpenVPN.ConfigurationParser.parsed(fromLines: ["scramble xormask FFFF"]) + XCTAssertNil(cfg.warning) + XCTAssertNil(cfg2.configuration.xorMask) + } + private func privateTestEncryptedCertificateKey(pkcs: String) throws { let cfgURL = url(withName: "tunnelbear.enc.\(pkcs)") XCTAssertThrowsError(try OpenVPN.ConfigurationParser.parsed(fromURL: cfgURL))