diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2024-01-08 10:03:25 +0100 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2024-01-08 10:03:25 +0100 |
| commit | aca939c260f718e3806a67b0a0703a33d4c8157f (patch) | |
| tree | 02d7d5e94b9429fd53c6f34e22559fd962320872 | |
| parent | 9d98bd30203a11728f9d90f444136af94bf32ea9 (diff) | |
| parent | db72d9b4089c0cd807a1ef938a18b5ef640e3c5a (diff) | |
| download | mullvadvpn-aca939c260f718e3806a67b0a0703a33d4c8157f.tar.xz mullvadvpn-aca939c260f718e3806a67b0a0703a33d4c8157f.zip | |
Merge branch 'add-authentication-support-for-socks5-access-methods-ios-417'
11 files changed, 205 insertions, 24 deletions
diff --git a/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift index 87624656f9..986f8276fa 100644 --- a/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift +++ b/ios/MullvadREST/Transport/Socks5/AnyIPEndpoint+Socks5.swift @@ -20,4 +20,14 @@ extension AnyIPEndpoint { .ipv6(endpoint) } } + + /// Convert `AnyIPEndpoint` to `NWEndpoint`. + var nwEndpoint: NWEndpoint { + switch self { + case let .ipv4(endpoint): + .hostPort(host: .ipv4(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port)) + case let .ipv6(endpoint): + .hostPort(host: .ipv6(endpoint.ip), port: NWEndpoint.Port(integerLiteral: endpoint.port)) + } + } } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift index 39a240d41e..a72dfc6108 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Authentication.swift @@ -6,9 +6,66 @@ // import Foundation +import Network /// Authentication methods supported by socks protocol. enum Socks5AuthenticationMethod: UInt8 { case notRequired = 0x00 case usernamePassword = 0x02 } + +struct Socks5Authentication { + let connection: NWConnection + let endpoint: Socks5Endpoint + let configuration: Socks5Configuration + + typealias AuthenticationComplete = () -> Void + typealias AuthenticationFailure = (Error) -> Void + + func authenticate(onComplete: @escaping AuthenticationComplete, onFailure: @escaping AuthenticationFailure) { + guard let username = configuration.username, let password = configuration.password else { + onFailure(Socks5Error.invalidUsernameOrPassword) + return + } + let authenticateCommand = Socks5UsernamePasswordCommand(username: username, password: password) + + connection.send(content: authenticateCommand.rawData, completion: .contentProcessed { error in + if let error { + onFailure(error) + } else { + readNegotiationReply(onComplete: onComplete, onFailure: onFailure) + } + }) + } + + func readNegotiationReply( + onComplete: @escaping AuthenticationComplete, + onFailure: @escaping AuthenticationFailure + ) { + let replySize = MemoryLayout<Socks5UsernamePasswordReply>.size + + // Read in one shot, the payload is very small to not care about a reading loop. + connection.receive(exactLength: replySize) { data, _, _, error in + guard let data else { + if let error { + onFailure(error) + } else { + onFailure(Socks5Error.unexpectedEndOfStream) + } + return + } + + guard let reply = Socks5UsernamePasswordReply(from: data) else { + onFailure(Socks5Error.unexpectedEndOfStream) + return + } + + guard reply.version == Socks5Constants.usernamePasswordAuthenticationProtocol else { + onFailure(Socks5Error.invalidSocksVersion) + return + } + + onComplete() + } + } +} diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift index 2ec3a94b00..4f7ebc06f9 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Configuration.swift @@ -8,25 +8,19 @@ import Foundation import MullvadTypes -import Network /// Socks5 configuration. /// - See: ``URLSessionSocks5Transport`` public struct Socks5Configuration { - public let address: AnyIPAddress - public let port: UInt16 + /// The socks proxy endpoint. + public var proxyEndpoint: AnyIPEndpoint - public init(address: AnyIPAddress, port: UInt16) { - self.address = address - self.port = port - } + public var username: String? + public var password: String? - var nwEndpoint: NWEndpoint { - switch self.address { - case let .ipv4(endpoint): - .hostPort(host: .ipv4(endpoint), port: NWEndpoint.Port(integerLiteral: port)) - case let .ipv6(endpoint): - .hostPort(host: .ipv6(endpoint), port: NWEndpoint.Port(integerLiteral: port)) - } + public init(proxyEndpoint: AnyIPEndpoint, username: String? = nil, password: String? = nil) { + self.proxyEndpoint = proxyEndpoint + self.username = username + self.password = password } } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift index a3163029fe..be232f8437 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Connection.swift @@ -12,7 +12,7 @@ import Network final class Socks5Connection { /// The remote endpoint to which the client wants to establish connection over the socks proxy. let remoteServerEndpoint: Socks5Endpoint - + let configuration: Socks5Configuration /** Initializes a new connection passing data between local and remote TCP connection over the socks proxy. @@ -26,12 +26,14 @@ final class Socks5Connection { queue: DispatchQueue, localConnection: NWConnection, socksProxyEndpoint: NWEndpoint, - remoteServerEndpoint: Socks5Endpoint + remoteServerEndpoint: Socks5Endpoint, + configuration: Socks5Configuration ) { self.queue = queue self.remoteServerEndpoint = remoteServerEndpoint self.localConnection = localConnection self.remoteConnection = NWConnection(to: socksProxyEndpoint, using: .tcp) + self.configuration = configuration } /** @@ -173,7 +175,10 @@ final class Socks5Connection { /// Start handshake with the socks proxy. private func sendHandshake() { - let handshake = Socks5Handshake() + var handshake = Socks5Handshake() + if configuration.username != nil && configuration.password != nil { + handshake.methods.append(.usernamePassword) + } let negotiation = Socks5HandshakeNegotiation( connection: remoteConnection, handshake: handshake, @@ -191,8 +196,18 @@ final class Socks5Connection { connect() case .usernamePassword: - // TODO: handle authentication - break + // Username + password authentication sends the data in plain text to the server + // And then continues like the `notRequired` case after the server has authenticated the client. + let authentication = Socks5Authentication( + connection: remoteConnection, + endpoint: remoteServerEndpoint, + configuration: configuration + ) + authentication.authenticate(onComplete: { [self] in + connect() + }, onFailure: { [self] error in + handleError(error) + }) } } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift index 325c7527da..f62642d270 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Constants.swift @@ -10,4 +10,6 @@ import Foundation enum Socks5Constants { /// Socks version. static let socksVersion: UInt8 = 0x05 + + static let usernamePasswordAuthenticationProtocol: UInt8 = 0x01 } diff --git a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift index 992c402160..8be764cbb1 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5Error.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5Error.swift @@ -34,6 +34,9 @@ public enum Socks5Error: Error { /// Server replied with unsupported authentication method. case unsupportedAuthMethod + /// Invalid username or password was provided to the server + case invalidUsernameOrPassword + /// None of the auth methods listed by the client are acceptable. case unacceptableAuthMethods diff --git a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift index 0b26bc3b92..29a1b2f70a 100644 --- a/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift +++ b/ios/MullvadREST/Transport/Socks5/Socks5ForwardingProxy.swift @@ -26,6 +26,8 @@ public final class Socks5ForwardingProxy { /// Remote server that socks proxy should connect to. public let remoteServerEndpoint: Socks5Endpoint + public let configuration: Socks5Configuration + /// Local TCP port that clients should use to communicate with the remote server. /// This property is set once the proxy is successfully started. public var listenPort: UInt16? { @@ -46,9 +48,14 @@ public final class Socks5ForwardingProxy { - socksProxyEndpoint: socks proxy endpoint. - remoteServerEndpoint: remote server that socks proxy should connect to. */ - public init(socksProxyEndpoint: NWEndpoint, remoteServerEndpoint: Socks5Endpoint) { + public init( + socksProxyEndpoint: NWEndpoint, + remoteServerEndpoint: Socks5Endpoint, + configuration: Socks5Configuration + ) { self.socksProxyEndpoint = socksProxyEndpoint self.remoteServerEndpoint = remoteServerEndpoint + self.configuration = configuration } deinit { @@ -251,7 +258,8 @@ public final class Socks5ForwardingProxy { queue: queue, localConnection: connection, socksProxyEndpoint: socksProxyEndpoint, - remoteServerEndpoint: remoteServerEndpoint + remoteServerEndpoint: remoteServerEndpoint, + configuration: configuration ) socks5Connection.setStateHandler { [weak self] socks5Connection, state in if case let .stopped(error) = state { diff --git a/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift b/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift new file mode 100644 index 0000000000..42e50fe5d4 --- /dev/null +++ b/ios/MullvadREST/Transport/Socks5/Socks5UsernamePasswordCommand.swift @@ -0,0 +1,83 @@ +// +// Socks5UsernamePasswordCommand.swift +// MullvadREST +// +// Created by Marco Nikic on 2023-12-13. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import Foundation + +/** + The payload sent to the server is the following diagram + +----+------+----------+------+----------+ + |VER | ULEN | UNAME | PLEN | PASSWD | + +----+------+----------+------+----------+ + | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + +----+------+----------+------+----------+ + + VER: The current version of this method, always 1 + ULEN: The length of `username` + UNAME: The username + PLEN: The length of `password` + PASSWD: The password + + **/ +struct Socks5UsernamePasswordCommand { + let username: String + let password: String + + var rawData: Data { + var data = Data() + guard username.count < UInt8.max, + password.count < UInt8.max, + let usernameData = username.data(using: .utf8), + let passwordData = password.data(using: .utf8) + else { return data } + + // Protocol version + data.append(Socks5Constants.usernamePasswordAuthenticationProtocol) + + // Username length + data.append(UInt8(username.count)) + + // Username + data.append(usernameData) + + // Password length + data.append(UInt8(password.count)) + + // Password + data.append(passwordData) + + return data + } +} + +/** + The expected answer payload looks like this + +-----+--------+ + | VER | STATUS | + +-----+--------+ + | 1 | 1 | + +-----+--------+ + */ +struct Socks5UsernamePasswordReply { + let version: UInt8 + let status: Socks5StatusCode + + /// - Parameter data: The bytes read from the network connection sent by a socks5 server as a reply to a `Socks5UsernamePasswordCommand`. + init?(from data: Data) { + let expectedSize = MemoryLayout<Self>.size + guard data.count == expectedSize else { return nil } + var iterator = data.makeIterator() + + guard let readVersion = iterator.next(), + readVersion == Socks5Constants.usernamePasswordAuthenticationProtocol else { return nil } + self.version = readVersion + + guard let readStatus = iterator.next(), + let statusCode = Socks5StatusCode(rawValue: readStatus) else { return nil } + self.status = statusCode + } +} diff --git a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift index 89121db0d0..6c3a67159b 100644 --- a/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift +++ b/ios/MullvadREST/Transport/Socks5/URLSessionSocks5Transport.swift @@ -45,8 +45,9 @@ public class URLSessionSocks5Transport: RESTTransport { let apiAddress = addressCache.getCurrentEndpoint() socksProxy = Socks5ForwardingProxy( - socksProxyEndpoint: configuration.nwEndpoint, - remoteServerEndpoint: apiAddress.socksEndpoint + socksProxyEndpoint: configuration.proxyEndpoint.nwEndpoint, + remoteServerEndpoint: apiAddress.socksEndpoint, + configuration: configuration ) socksProxy.setErrorHandler { [weak self] error in diff --git a/ios/MullvadREST/Transport/TransportProvider.swift b/ios/MullvadREST/Transport/TransportProvider.swift index d0baf7c092..e88afd33dc 100644 --- a/ios/MullvadREST/Transport/TransportProvider.swift +++ b/ios/MullvadREST/Transport/TransportProvider.swift @@ -78,10 +78,14 @@ public final class TransportProvider: RESTTransportProvider { } } + // TODO: Pass the socks5 username, password, and ip+port combo here. private func socks5() -> RESTTransport? { return URLSessionSocks5Transport( urlSession: urlSessionTransport.urlSession, - configuration: Socks5Configuration(address: .ipv4(.loopback), port: 8889), + configuration: Socks5Configuration(proxyEndpoint: AnyIPEndpoint.ipv4(IPv4Endpoint( + ip: .loopback, + port: 8889 + ))), addressCache: addressCache ) } diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 2db19e7335..60a74fa1b5 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -599,6 +599,7 @@ A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; }; A94D691A2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E22AA72AE9003D1918 /* WireGuardKitTypes */; }; A94D691B2ABAD66700413DD4 /* WireGuardKitTypes in Frameworks */ = {isa = PBXBuildFile; productRef = 58FE25E72AA7399D003D1918 /* WireGuardKitTypes */; }; + A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */; }; A97D25AE2B0BB18100946B2D /* ProtocolObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */; }; A97D25B02B0BB5C400946B2D /* ProtocolObfuscationStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */; }; A97D25B22B0CB02D00946B2D /* ProtocolObfuscatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */; }; @@ -1730,6 +1731,7 @@ A92ECC272A7802AB0052F1B1 /* StoredDeviceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredDeviceData.swift; sourceTree = "<group>"; }; A92ECC2B2A7803A50052F1B1 /* DeviceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceState.swift; sourceTree = "<group>"; }; A9467E7E2A29DEFE000DC21F /* RelayCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayCacheTests.swift; sourceTree = "<group>"; }; + A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Socks5UsernamePasswordCommand.swift; sourceTree = "<group>"; }; A97D25AD2B0BB18100946B2D /* ProtocolObfuscator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscator.swift; sourceTree = "<group>"; }; A97D25AF2B0BB5C400946B2D /* ProtocolObfuscationStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscationStub.swift; sourceTree = "<group>"; }; A97D25B12B0CB02D00946B2D /* ProtocolObfuscatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolObfuscatorTests.swift; sourceTree = "<group>"; }; @@ -3282,6 +3284,7 @@ A90763AF2B2857D50045ADF0 /* Socks5Handshake.swift */, A90763AE2B2857D50045ADF0 /* Socks5HandshakeNegotiation.swift */, A90763AC2B2857D50045ADF0 /* Socks5StatusCode.swift */, + A970C89C2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift */, A90763C02B2858310045ADF0 /* URLSessionSocks5Transport.swift */, ); path = Socks5; @@ -4278,6 +4281,7 @@ A90763B02B2857D50045ADF0 /* Socks5ConnectCommand.swift in Sources */, 06799ADD28F98E4800ACD94E /* RESTError.swift in Sources */, A90763B92B2857D50045ADF0 /* Socks5ForwardingProxy.swift in Sources */, + A970C89D2B29E38C000A7684 /* Socks5UsernamePasswordCommand.swift in Sources */, A90763B32B2857D50045ADF0 /* Socks5Authentication.swift in Sources */, 06799ADB28F98E4800ACD94E /* RESTProxyFactory.swift in Sources */, F0DDE4182B220458006B57A7 /* ShadowsocksConfiguration.swift in Sources */, |
